diff --git a/404.html b/404.html index ec5e1eca..0d66b467 100644 --- a/404.html +++ b/404.html @@ -1 +1 @@ -
共计 19 篇文章
2020
着色器之书06 着色器之书05 着色器之书04 着色器之书03 着色器之书02 着色器之书01 着色器之书00 重新编译nginx添加新模块的方法 使用coding来作为图床的方法共计 19 篇文章
2020
着色器之书06 着色器之书05 着色器之书04 着色器之书03 着色器之书02 着色器之书01 着色器之书00 重新编译nginx添加新模块的方法 使用coding来作为图床的方法本文最后更新于:10 天前
体验地址: https://www.aigisss.com/view/#/login
1 |
|
1 |
|
1 |
|
qq比较特别,其余的按照网上说明配置即可成功,下面着重说明QQ的一些配置.
先申请QQ互联地址->注册认证开发者->创建应用等待审核
填写回调的时候需要注意
需要填写一个不带斜杠的地址,不然通过不了,说是回调地址不合法:须为http或https开头的子目录。如http://qq.com/mycb
特别感谢子钦加油
https://www.cnblogs.com/zmdComeOn/p/12667228.html
https://blog.csdn.net/weixin_39944891/article/details/94739204
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 分钟前
体验地址: https://www.aigisss.com/view/#/login
1 |
|
1 |
|
1 |
|
qq比较特别,其余的按照网上说明配置即可成功,下面着重说明QQ的一些配置.
先申请QQ互联地址->注册认证开发者->创建应用等待审核
填写回调的时候需要注意
需要填写一个不带斜杠的地址,不然通过不了,说是回调地址不合法:须为http或https开头的子目录。如http://qq.com/mycb
特别感谢子钦加油
https://www.cnblogs.com/zmdComeOn/p/12667228.html
https://blog.csdn.net/weixin_39944891/article/details/94739204
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 年前
vue-cli-service-build报错No module factory available for dependency type: CssDependency
https://forum.vuejs.org/t/vue-cli-service-build/83951
在vue.config.js
文件中配置
1 |
|
原因看下面
https://cli.vuejs.org/config/#css-extract
https://segmentfault.com/a/1190000016390112
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 年前
vue-cli-service-build报错No module factory available for dependency type: CssDependency
https://forum.vuejs.org/t/vue-cli-service-build/83951
在vue.config.js
文件中配置
1 |
|
原因看下面
https://cli.vuejs.org/config/#css-extract
https://segmentfault.com/a/1190000016390112
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:7 个月前
1 |
|
1 |
|
打开postgresql
的官网安装教程:https://www.postgresql.org/download/linux/redhat/ ,选择一下系统
就会发现安装如下的命令行:
1 |
|
1 |
|
1 |
|
1 |
|
编辑内容如下:
1 |
|
source /etc/profile
更新配置!
1 |
|
1 |
|
修改postgres/data
目录下的pg_hba.conf
1 |
|
修改IPv4
一行内容如下:
1 |
|
修改postgresql.conf
:
1 |
|
修改监听一节如下:
1 |
|
wq!
保存退出。
重启pg服务生效
1 |
|
创建 postgresql.service
服务脚本
1 |
|
编辑内容如下:
1 |
|
如果之前使用pg_ctl restart -D $PGDATA
启动过,先停止pg_ctl stop -D $PGDATA
更新设置:
1 |
|
配置开机自启动:
1 |
|
启动服务
1 |
|
停止服务
1 |
|
重启服务
1 |
|
使服务自动启动
1 |
|
使服务不自动启动
1 |
|
检查服务状态
1 |
|
显示所有已启动的服务
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:7 个月前
1 |
|
1 |
|
打开postgresql
的官网安装教程:https://www.postgresql.org/download/linux/redhat/ ,选择一下系统
就会发现安装如下的命令行:
1 |
|
1 |
|
1 |
|
1 |
|
编辑内容如下:
1 |
|
source /etc/profile
更新配置!
1 |
|
1 |
|
修改postgres/data
目录下的pg_hba.conf
1 |
|
修改IPv4
一行内容如下:
1 |
|
修改postgresql.conf
:
1 |
|
修改监听一节如下:
1 |
|
wq!
保存退出。
重启pg服务生效
1 |
|
创建 postgresql.service
服务脚本
1 |
|
编辑内容如下:
1 |
|
如果之前使用pg_ctl restart -D $PGDATA
启动过,先停止pg_ctl stop -D $PGDATA
更新设置:
1 |
|
配置开机自启动:
1 |
|
启动服务
1 |
|
停止服务
1 |
|
重启服务
1 |
|
使服务自动启动
1 |
|
使服务不自动启动
1 |
|
检查服务状态
1 |
|
显示所有已启动的服务
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
https://www.shadertoy.com/view/lsX3W4
https://www.shadertoy.com/view/Mss3Wf
https://www.shadertoy.com/view/4df3Rn
https://www.shadertoy.com/view/Mss3R8
https://www.shadertoy.com/view/4dfGRn
https://www.shadertoy.com/view/lss3zs
https://www.shadertoy.com/view/4dXGDX
https://www.shadertoy.com/view/XsXGz2
https://www.shadertoy.com/view/lls3D7
https://www.shadertoy.com/view/XdB3DD
https://www.shadertoy.com/view/XdBSWw
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
https://www.shadertoy.com/view/lsX3W4
https://www.shadertoy.com/view/Mss3Wf
https://www.shadertoy.com/view/4df3Rn
https://www.shadertoy.com/view/Mss3R8
https://www.shadertoy.com/view/4dfGRn
https://www.shadertoy.com/view/lss3zs
https://www.shadertoy.com/view/4dXGDX
https://www.shadertoy.com/view/XsXGz2
https://www.shadertoy.com/view/lls3D7
https://www.shadertoy.com/view/XdB3DD
https://www.shadertoy.com/view/XdBSWw
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:5 个月前
111
因为使用gzip
压缩时,我将gzip_static
开启,结果出错了.
1 |
|
说是没有安装这个模块,但是我已经安装了!!
下面讲解如何在已经安装过后再次添加新的模块。
因为源码已经删掉了,重新下载一份
1 |
|
1 |
|
1 |
|
千万别执行
make install
make
完之后在当前目录下的/objs
目录下就多了个nginx
,这个就是新版本的程序了。
以防万一,备份旧的程序
1 |
|
将新的nginx
程序复制到/usr/local/nginx/sbin/
下
1 |
|
1 |
|
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:5 个月前
111
因为使用gzip
压缩时,我将gzip_static
开启,结果出错了.
1 |
|
说是没有安装这个模块,但是我已经安装了!!
下面讲解如何在已经安装过后再次添加新的模块。
因为源码已经删掉了,重新下载一份
1 |
|
1 |
|
1 |
|
千万别执行
make install
make
完之后在当前目录下的/objs
目录下就多了个nginx
,这个就是新版本的程序了。
以防万一,备份旧的程序
1 |
|
将新的nginx
程序复制到/usr/local/nginx/sbin/
下
1 |
|
1 |
|
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
在经历了众多的重复与秩序之后,笔者自然而然地开始着手创造一些混沌。
随机性是熵的最大表现。我们如何在看似可预测而且严苛的代码环境中生成随机性呢?
让我们从分析下面的函数着手:
上面的例子中,我们提取了sin函数其波形的分数部分。值域为-1.0
到 1.0
之间的sin()
函数被取了小数点后的部分(这里实际是指模1)),返回0.0
到 1.0
间的正值。我们可以用这种效果通过把正弦函数打散成小片段来得到一些伪随机数。如何实现呢?通过在sin(x)
的值上乘以大些的数。点击上面的函数,在1后面加些0。
当你加到 100000.0
(方程看起来是这样的:y = fract(sin(x)*100000.0)
),你再也区分不出sin波了。小数部分的粒度将sine的循环变成了伪随机的混沌。
使用随机会很难;它不是太混沌难测就是有时又不够混乱。看看下面的图例。要实现这样的效果,我们像之前描述的那样应用用 rand()
函数。
细看,你可以看到 sin()
在 -1.5707
和 1.5707
上有较大波动。我打赌你现在一定理解这是为什么——那就是sin取得最大值和最小值的地方。
如果你仔细观察随机分布,你会注意到相比边缘,中部更集中。
本文最后更新于:1 个月前
在经历了众多的重复与秩序之后,笔者自然而然地开始着手创造一些混沌。
随机性是熵的最大表现。我们如何在看似可预测而且严苛的代码环境中生成随机性呢?
让我们从分析下面的函数着手:
上面的例子中,我们提取了sin函数其波形的分数部分。值域为-1.0
到 1.0
之间的sin()
函数被取了小数点后的部分(这里实际是指模1)),返回0.0
到 1.0
间的正值。我们可以用这种效果通过把正弦函数打散成小片段来得到一些伪随机数。如何实现呢?通过在sin(x)
的值上乘以大些的数。点击上面的函数,在1后面加些0。
当你加到 100000.0
(方程看起来是这样的:y = fract(sin(x)*100000.0)
),你再也区分不出sin波了。小数部分的粒度将sine的循环变成了伪随机的混沌。
使用随机会很难;它不是太混沌难测就是有时又不够混乱。看看下面的图例。要实现这样的效果,我们像之前描述的那样应用用 rand()
函数。
细看,你可以看到 sin()
在 -1.5707
和 1.5707
上有较大波动。我打赌你现在一定理解这是为什么——那就是sin取得最大值和最小值的地方。
如果你仔细观察随机分布,你会注意到相比边缘,中部更集中。
不久前 Pixelero 发表了一篇关于随机分布的有意思的文章。 我添加了些前几张图所有的函数来供你试验,看看如何改变分布。取消函数的注释,看看发生什么变化。
如果你读下 Pixelero 的文章,一定谨记我们用的 rand()
是确定性随机,也被称作是伪随机。这就意味着, 就 rand(1.)
为例,总会返回相同的值。Pixelero 用 ActionSript 函数做了些参考,Math.random()
,一个非确定性随机;每次调用都返回不同的值。
现在我们对随机有了深入的理解,是时候将它应用到二维,x
轴和 y
轴。为此我们需要将一个二维向量转化为一维浮点数。这里有几种不同的方法来实现,但 dot()
函数在这个例子中尤其有用。它根据两个向量的方向返回一个 0.0
到 1.0
之间的值。
看下第13行和15行,注意我们如何将 vec2 st
和另一个二维向量 ( vec2(12.9898,78.233)
)。
试着改变14和15行的值。看看随机图案的变化,想想从中我们能学到什么。
好好用鼠标(u_mouse
)和时间(u_time
)调戏这个随机函数,更好的理解它如何工作。
二维的随机看起来是不是像电视的噪点?对组成图像来说,随机是个难用的原始素材。让我们来学着如何来利用它。
我们的第一步是在网格上的应用;用 floor()
函数,我们将会产生一个单元整数列表。看下下面的代码,尤其是22行和23行。
在缩放空间10倍后(在21行),我们将坐标系统的整数和小数部分分离。我们对最后一步操作不陌生,因为我们曾经用这种方法来将空间细分成 0.0
到 1.0
的小单元。我们根据得到坐标的整数部分作为一个通用值来隔离一个区域的像素,让它看起来像个单独的单元。然后我们可以用这个通用值来为这个区域得到一个随机值。因为我们的随机函数是伪随机,在那个单元内的所有像素返回的随机值都是一个常量。
取消第29行保留我们坐标的小数部分,这样我们仍旧可以将其用作一个坐标系统,来在单元内部画图形。
结合这两个量 — 坐标的整数部分和小数部分 — 将使你可以结合变化和秩序。
看下这个著名的 10 PRINT CHR$(205.5+RND(1)); : GOTO 10
迷宫生成器的GLSL代码块。
这里我用前一章的 truchetPattern()
函数根据单元产生的随机值来随机画一个方向的对角线。
你可以通过取消50到53行的代码块的注释得到其他有趣的图案,或者通过取消35和36行来得到图案的动画。
Ryoji Ikeda,日本电子作曲家、视觉艺术家,是运用随机的大师;他的作品是如此的富有感染力而难忘。他在音乐和视觉媒介中随机的运用,不再是混乱无序,反而以假乱真地折射出我们技术文化的复杂性。
看看 Ikeda 的作品并试试看下面的练习:
完美地掌握随机之美是困难的,尤其是你想要让作品看起来很自然。随机仅仅是过于混乱了,真实生活中很少有东西看上去如此 random()
。如果观察(玻璃床上)雨滴的肌理或是股票的曲线——这两个都挺随机的——但他们和本章开始的随机图案看起来不是同一对爹妈生的。原因?嗯,随机值之间没有什么相关性,而大多数自然图案(肌理)都对前一个状态有所记忆(基于前一个状态)。
下一章我们将学习噪声,一种光滑 和 自然的 创作计算机混沌的方式。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
现在你可能跃跃欲试,想在你熟悉的平台上小试牛刀了。接下来会有一些比较流行的平台的示例代码,展示如何在这些平台上配置 shader。(在这个 github 仓库 中有本章的三种平台的示例代码)
注释 1:如果你不想用这些平台来运行 shader,且你想在浏览器外使用 shader,你可以下载glslViewer。这个 MacOS+树莓派程序直接在终端运行,并且是为本书的例子量身打造的。
注释2:如果你想用 WebGL 显示 shader,并不关心其他平台,你可以用glslCanvas 。这个 web 工具本来是为本书设计的,但是太好用了,所以我常常用在其他项目中。
为人谦逊而非常有才华的 Ricardo Cabello (也就是 MrDoob )和许多贡献者 一起搭了可能是 WebGL 最知名的平台,Three.js。你可以找到无数程序示例,教程,书籍,教你如何用这个 JavaScript 库做出酷炫的 3D 图像。
下面是一个你需要的例子,教你用 three.js 玩转 shader。注意 id="fragmentShader"
脚本,你要把下面的代码拷到里面。
下面是一个 HTML 和 JS 的示例,
1 |
|
2001年由Ben Fry 和 Casey Reas 创建,Processing是一个极其简约而强大的环境,非常适合初尝代码的人(至少对于我来是这样)。关于 OpenGL 和视频,Andres Colubri为 Processing 平台做了很重要的更新,使得环境非常友好,玩 GLSL shader 比起以前大大容易了。Processing 会在你的 sketch 的 data
文件夹搜索名为 "shader.frag"
的文件。记得把这里的示例代码拷到你的文件夹里然后重命名 shader。
1 |
|
在 2.1 版之前的版本运行 shader,你需要在你的 shader 文件开头添加以下代码:
1 |
|
1 |
|
更多 Processing 的 shader 教程戳 tutorial。
每个人都有自己的舒适区,我的则是openFrameworks community。这个 C++ 框架打包了 OpenGL 和其他开源 C++ 库。在很多方面它和 Processing 非常像,但是明显和 C++ 编译器打交道一定比较麻烦。和 Processing 很像地,openFrameworks 会在你的 data 文件夹里寻找 shader 文件,所以不要忘记把你的后缀 .frag
的文件拷进去,加载的时候记得改名。
1 |
|
关于 shader 在 openFrameworks 的更多信息请参考这篇excellent tutorial,作者是 Joshua Noble。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
现在你可能跃跃欲试,想在你熟悉的平台上小试牛刀了。接下来会有一些比较流行的平台的示例代码,展示如何在这些平台上配置 shader。(在这个 github 仓库 中有本章的三种平台的示例代码)
注释 1:如果你不想用这些平台来运行 shader,且你想在浏览器外使用 shader,你可以下载glslViewer。这个 MacOS+树莓派程序直接在终端运行,并且是为本书的例子量身打造的。
注释2:如果你想用 WebGL 显示 shader,并不关心其他平台,你可以用glslCanvas 。这个 web 工具本来是为本书设计的,但是太好用了,所以我常常用在其他项目中。
为人谦逊而非常有才华的 Ricardo Cabello (也就是 MrDoob )和许多贡献者 一起搭了可能是 WebGL 最知名的平台,Three.js。你可以找到无数程序示例,教程,书籍,教你如何用这个 JavaScript 库做出酷炫的 3D 图像。
下面是一个你需要的例子,教你用 three.js 玩转 shader。注意 id="fragmentShader"
脚本,你要把下面的代码拷到里面。
下面是一个 HTML 和 JS 的示例,
1 |
|
2001年由Ben Fry 和 Casey Reas 创建,Processing是一个极其简约而强大的环境,非常适合初尝代码的人(至少对于我来是这样)。关于 OpenGL 和视频,Andres Colubri为 Processing 平台做了很重要的更新,使得环境非常友好,玩 GLSL shader 比起以前大大容易了。Processing 会在你的 sketch 的 data
文件夹搜索名为 "shader.frag"
的文件。记得把这里的示例代码拷到你的文件夹里然后重命名 shader。
1 |
|
在 2.1 版之前的版本运行 shader,你需要在你的 shader 文件开头添加以下代码:
1 |
|
1 |
|
更多 Processing 的 shader 教程戳 tutorial。
每个人都有自己的舒适区,我的则是openFrameworks community。这个 C++ 框架打包了 OpenGL 和其他开源 C++ 库。在很多方面它和 Processing 非常像,但是明显和 C++ 编译器打交道一定比较麻烦。和 Processing 很像地,openFrameworks 会在你的 data 文件夹里寻找 shader 文件,所以不要忘记把你的后缀 .frag
的文件拷进去,加载的时候记得改名。
1 |
|
关于 shader 在 openFrameworks 的更多信息请参考这篇excellent tutorial,作者是 Joshua Noble。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:7 个月前
遇到hexo编译之后发布不成功,报Permission denied
的错!
1 |
|
解决方法:
使用git bash解决了,会弹出密码输入框,填入就好了!
参考文章:https://blog.csdn.net/dingding_12345/article/details/69666233
又遇到RPC failed; curl 56 OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 10054
的错:
1 |
|
1 |
|
在主题下的_config.yml
文件中设置:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:7 个月前
遇到hexo编译之后发布不成功,报Permission denied
的错!
1 |
|
解决方法:
使用git bash解决了,会弹出密码输入框,填入就好了!
参考文章:https://blog.csdn.net/dingding_12345/article/details/69666233
又遇到RPC failed; curl 56 OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 10054
的错:
1 |
|
1 |
|
在主题下的_config.yml
文件中设置:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
这是一本关于 Fragment Shaders(片段着色器)的入门指南,它将一步一步地带你领略其中的纷繁与抽象。
这本书是 Patricio 的 the Book of Shaders 的中文翻译。我们希望借此将 Shader 这个有趣有益的工具介绍给更多国人。能力所限,不免有误,如有翻译不当,也请多多指出。
感谢 Patricio 对我们的翻译的信任和支持。
Patricio Gonzalez Vivo (1982, 布宜诺斯艾利斯, 阿根廷) 是一个驻地纽约的艺术家、开发者。他致力于探索有机和人造、模拟信号和数字信号、个体和整体之间的空间。他用代码这种富有表达力的语言来创造更美好的事物。
Patricio 研习和实践精神疗法(psychotherapy)和表达性艺术治疗(expressive art therapy)。他毕业于 Parsons 的设计与科技专业,且目前执教于此。目前他作为 Mapzen 的图形开发工程师制作一些开源的 mapping tool。
后续章节作者仍在撰写中,如果感兴趣可以在 github 上查看部分后续章节代码。
感谢我的妻子 Jen Lowe, 感谢她无条件的支持、帮助以及编辑此书。
感谢 Scott Murray 给予的启发和建议。
感谢 Kenichi Yoneda (Kynd) 和 Sawako 的 日文版翻译(日本語訳)
感谢 Tong Li 和 Yi Zhang 的 中文版(Chinese) 翻译。
感谢 Jae Hyun Yoo 的 韩文版 (한국어) 翻译。
感谢 Nahuel Coppero (Necsoft) 的 西班牙语(español) 翻译。
感谢 Karim Naaji 在代码和想法上的支持和贡献。
感谢所有相信这个项目的人contributed with fixes 以及大家的捐赠.
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
这是一本关于 Fragment Shaders(片段着色器)的入门指南,它将一步一步地带你领略其中的纷繁与抽象。
这本书是 Patricio 的 the Book of Shaders 的中文翻译。我们希望借此将 Shader 这个有趣有益的工具介绍给更多国人。能力所限,不免有误,如有翻译不当,也请多多指出。
感谢 Patricio 对我们的翻译的信任和支持。
Patricio Gonzalez Vivo (1982, 布宜诺斯艾利斯, 阿根廷) 是一个驻地纽约的艺术家、开发者。他致力于探索有机和人造、模拟信号和数字信号、个体和整体之间的空间。他用代码这种富有表达力的语言来创造更美好的事物。
Patricio 研习和实践精神疗法(psychotherapy)和表达性艺术治疗(expressive art therapy)。他毕业于 Parsons 的设计与科技专业,且目前执教于此。目前他作为 Mapzen 的图形开发工程师制作一些开源的 mapping tool。
后续章节作者仍在撰写中,如果感兴趣可以在 github 上查看部分后续章节代码。
感谢我的妻子 Jen Lowe, 感谢她无条件的支持、帮助以及编辑此书。
感谢 Scott Murray 给予的启发和建议。
感谢 Kenichi Yoneda (Kynd) 和 Sawako 的 日文版翻译(日本語訳)
感谢 Tong Li 和 Yi Zhang 的 中文版(Chinese) 翻译。
感谢 Jae Hyun Yoo 的 韩文版 (한국어) 翻译。
感谢 Nahuel Coppero (Necsoft) 的 西班牙语(español) 翻译。
感谢 Karim Naaji 在代码和想法上的支持和贡献。
感谢所有相信这个项目的人contributed with fixes 以及大家的捐赠.
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 年前
出现这种情况,发现静态资源都加载不到,后来网上搜索了半天,才发现原来你要打算用 coding 的 pages 服务部署你的博客的话,你创建项目的名字必须和用户名保持一致,不能自己随便自定义。我重新创建了一个和用户名一致的项目,部署到他的 pages 服务,访问正常
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 年前
出现这种情况,发现静态资源都加载不到,后来网上搜索了半天,才发现原来你要打算用 coding 的 pages 服务部署你的博客的话,你创建项目的名字必须和用户名保持一致,不能自己随便自定义。我重新创建了一个和用户名一致的项目,部署到他的 pages 服务,访问正常
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
之前的章节我们学习了如何制作一些图形 - 而如何移动它们的技巧则是借助移动它们自身的参考坐标系。我们只需要给 st
变量加上一个包含每个片段的位置的向量。这样就移动了整个坐标系。
还是画着比较更容易解释,如上图所示:
现在尝试下下面的练习:
u_time
和造型函数来移动十字,并试着让它有趣一点。找一个你觉得你感兴趣的某种运动形式,让这个十字也这样运动。记录“真实世界”的一些现象或许对你有所启发 — 可以是波的运动,摆动,弹球,汽车的加速运动,一辆自行车的刹车。要移动物体,我们同样需要移动整个空间(坐标)系统。为此我们将使用一个矩阵。矩阵是一个通过行和列定义的一组数。用矩阵乘以一个向量是用一组精确的规则定义的,这样做是为了以一组特定的方式来改变向量的值。
GLSL本身支持2维,3维和4维方阵(mm矩阵):mat2
(2x2), mat3
(3x3) 和 mat4
(4x4)。GLSL同样支持矩阵相乘 (```)和特殊矩阵函数([
matrixCompMult()```](../glossary/?search=matrixCompMult))。
基于矩阵的特性,我们便有可能构造一个矩阵来产生特定的作用。比如我们可以用一个矩阵来平移一个向量:
更有趣的是,我们可以用矩阵来旋转坐标系统:
看下下面构成2维旋转的矩阵的代码。这个函数根据上面的公式,将二维向量绕 vec2(0.0)
点旋转。
1 |
|
根据以往我们画形状的方式,这并不是我们想要的。我们的十字是画在画布中心的,对应于点 vec2(0.5)
。所以,再旋转坐标空间之前,我们需要先把图形移到中心点,坐标 vec2(0.0)
,再旋转坐标空间,最后在移动回原点。
就像下面的代码:
试试下面的练习:
取消第45行的代码,看看会发生什么。
在37行和39行,将旋转之前的平移注释掉,观察结果。
用旋转改进在平移练习中模拟的动画。
我们看到了如何用矩阵平移和旋转物体。(或者更准确的说,如何通过变换坐标系统来旋转和移动物体。)如果你用过3D建模软件或者 Processing中的 pushmatrix 和 popmatrix 函数,你会知道矩阵也可以被用来缩放物体的大小。
根据上面的公式,我们知道如何构造一个2D缩放矩阵:
1 |
|
试试下面的练习,尝试深入理解矩阵的工作机制:
取消上面代码中的第42行来观察空间坐标是如何被缩放的。
看看注释掉37和39行,变换之前和之后的缩放,会发生什么。
试着结合旋转矩阵和缩放矩阵。注意他们的先后顺序。先乘以一个矩阵,再乘以向量。
现在你知道如何画不同的图形,知道如何移动,旋转和缩放它们,是时候用这些来创作了。设计一个fake UI or HUD (heads up display)。参考Ndel在ShaderToy上的例子。
YUV 是个用来模拟照片和视频的编码的色彩空间。这个色彩空间考虑人类的感知,减少色度的带宽。
下面的代码展现一种利用GLSL中的矩阵操作来切换颜色模式的有趣可能。
正如你所见,我们用对向量乘以矩阵的方式对待色彩。用这种方式,我们“移动”这些值。
这章我们学习如何运用矩阵变换来移动,旋转和缩放向量。除了之前章节学的图形,这些变换是创作的基础。在接下来的章节我们会应用我们所学的制作漂亮的程序纹理。你会发现编程的重复性和多样性是种令人兴奋的实践。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 个月前
之前的章节我们学习了如何制作一些图形 - 而如何移动它们的技巧则是借助移动它们自身的参考坐标系。我们只需要给 st
变量加上一个包含每个片段的位置的向量。这样就移动了整个坐标系。
还是画着比较更容易解释,如上图所示:
现在尝试下下面的练习:
u_time
和造型函数来移动十字,并试着让它有趣一点。找一个你觉得你感兴趣的某种运动形式,让这个十字也这样运动。记录“真实世界”的一些现象或许对你有所启发 — 可以是波的运动,摆动,弹球,汽车的加速运动,一辆自行车的刹车。要移动物体,我们同样需要移动整个空间(坐标)系统。为此我们将使用一个矩阵。矩阵是一个通过行和列定义的一组数。用矩阵乘以一个向量是用一组精确的规则定义的,这样做是为了以一组特定的方式来改变向量的值。
GLSL本身支持2维,3维和4维方阵(mm矩阵):mat2
(2x2), mat3
(3x3) 和 mat4
(4x4)。GLSL同样支持矩阵相乘 (```)和特殊矩阵函数([
matrixCompMult()```](../glossary/?search=matrixCompMult))。
基于矩阵的特性,我们便有可能构造一个矩阵来产生特定的作用。比如我们可以用一个矩阵来平移一个向量:
更有趣的是,我们可以用矩阵来旋转坐标系统:
看下下面构成2维旋转的矩阵的代码。这个函数根据上面的公式,将二维向量绕 vec2(0.0)
点旋转。
1 |
|
根据以往我们画形状的方式,这并不是我们想要的。我们的十字是画在画布中心的,对应于点 vec2(0.5)
。所以,再旋转坐标空间之前,我们需要先把图形移到中心点,坐标 vec2(0.0)
,再旋转坐标空间,最后在移动回原点。
就像下面的代码:
试试下面的练习:
取消第45行的代码,看看会发生什么。
在37行和39行,将旋转之前的平移注释掉,观察结果。
用旋转改进在平移练习中模拟的动画。
我们看到了如何用矩阵平移和旋转物体。(或者更准确的说,如何通过变换坐标系统来旋转和移动物体。)如果你用过3D建模软件或者 Processing中的 pushmatrix 和 popmatrix 函数,你会知道矩阵也可以被用来缩放物体的大小。
根据上面的公式,我们知道如何构造一个2D缩放矩阵:
1 |
|
试试下面的练习,尝试深入理解矩阵的工作机制:
取消上面代码中的第42行来观察空间坐标是如何被缩放的。
看看注释掉37和39行,变换之前和之后的缩放,会发生什么。
试着结合旋转矩阵和缩放矩阵。注意他们的先后顺序。先乘以一个矩阵,再乘以向量。
现在你知道如何画不同的图形,知道如何移动,旋转和缩放它们,是时候用这些来创作了。设计一个fake UI or HUD (heads up display)。参考Ndel在ShaderToy上的例子。
YUV 是个用来模拟照片和视频的编码的色彩空间。这个色彩空间考虑人类的感知,减少色度的带宽。
下面的代码展现一种利用GLSL中的矩阵操作来切换颜色模式的有趣可能。
正如你所见,我们用对向量乘以矩阵的方式对待色彩。用这种方式,我们“移动”这些值。
这章我们学习如何运用矩阵变换来移动,旋转和缩放向量。除了之前章节学的图形,这些变换是创作的基础。在接下来的章节我们会应用我们所学的制作漂亮的程序纹理。你会发现编程的重复性和多样性是种令人兴奋的实践。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
本文最后更新于:1 年前
1 |
|
gitGraph:
+markdown的使用总结 - AIGISSS markdown的使用总结
本文最后更新于:1 年前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gitGraph:
options
{
"nodeSpacing": 150,
"nodeRadius": 10
}
end
commit
branch newbranch
checkout newbranch
commit
commit
checkout master
commit
commit
merge newbranch
gitGraph:
options
{
"nodeSpacing": 150,
diff --git a/posts/3d0c447.html b/posts/3d0c447.html
index c1e1f598..55e3d55e 100644
--- a/posts/3d0c447.html
+++ b/posts/3d0c447.html
@@ -1 +1 @@
-vue学习与总结 - AIGISSS vue学习与总结
本文最后更新于:1 年前
插值表达式
- v-cloak
- v-text
- v-html
使用 v-cloak 能够解决 插值表达式闪烁的问题,[v-cloak] {display: none;}
。默认 v-text 是没有闪烁问题的,v-text 会覆盖元素中原本的内容,但是插值表达式 只会替换自己的这个占位符,不会把整个元素的内容清空
v-bind(缩写:)
v-on(缩写@)
v-model 只能用于表单元素
v-for
v-if
v-show
一般来说,v-if 有更高的切换消耗而 v-show 有更高的初始渲染消耗。因此,如果需要频繁切换,v-show 较好,如果运行时条件不太可能改变 v-if 较好
事件修饰符
- .stop 阻止冒泡
- .prevent 阻止默认事件
- .capture 添加事件侦听器时使用事件捕获模式
- .self 只当事件在该元素本身(比如不是子元素)触发时触发回调
- .once 事件只触发一次
在 Vue 中使用样式
使用 class 样式
- 数组
1
<h1 :class="['red', 'thin']">这是一个邪恶的H1</h1>
- 数组中使用三元表达式
1
<h1 :class="['red', 'thin', isactive?'active':'']">这是一个邪恶的H1</h1>
- 数组中嵌套对象
1
<h1 :class="['red', 'thin', {'active': isactive}]">这是一个邪恶的H1</h1>
- 直接使用对象
1
<h1 :class="{red:true, italic:true, active:true, thin:true}">这是一个邪恶的H1</h1>
使用内联样式
- 直接在元素上通过
:style
的形式,书写样式对象
1
<h1 :style="{color: 'red', 'font-size': '40px'}">这是一个善良的H1</h1>
- 将样式对象,定义到
data
中,并直接引用到 :style
中
- 在 data 上定义样式:
1
2
3
data: {
h1StyleObj: { color: 'red', 'font-size': '40px', 'font-weight': '200' }
}
- 在元素中,通过属性绑定的形式,将样式对象应用到元素中:
1
<h1 :style="h1StyleObj">这是一个善良的H1</h1>
- 在
:style
中通过数组,引用多个 data
上的样式对象
- 在 data 上定义样式:
1
2
3
4
data: {
h1StyleObj: { color: 'red', 'font-size': '40px', 'font-weight': '200' },
h1StyleObj2: { fontStyle: 'italic' }
}
- 在元素中,通过属性绑定的形式,将样式对象应用到元素中:
1
<h1 :style="[h1StyleObj, h1StyleObj2]">这是一个善良的H1</h1>
Vue 指令之v-if
和v-show
一般来说,v-if 有更高的切换消耗而 v-show 有更高的初始渲染消耗。因此,如果需要频繁切换 v-show 较好,如果在运行时条件不大可能改变 v-if 较好。
过滤器
概念:Vue.js 允许你自定义过滤器,可被用作一些常见的文本格式化。过滤器可以用在两个地方:mustache 插值和 v-bind 表达式。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符指示;
私有过滤器
- HTML 元素:
- 私有
filters
定义方式:
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
filters: { // 私有局部过滤器,只能在 当前 VM 对象所控制的 View 区域进行使用
dataFormat(input, pattern = "") { // 在参数列表中 通过 pattern="" 来指定形参默认值,防止报错
var dt = new Date(input);
// 获取年月日
var y = dt.getFullYear();
var m = (dt.getMonth() + 1).toString().padStart(2, '0');
var d = dt.getDate().toString().padStart(2, '0');
// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日
// 否则,就返回 年-月-日 时:分:秒
if (pattern.toLowerCase() === 'yyyy-mm-dd') {
return `${y}-${m}-${d}`;
} else {
// 获取时分秒
var hh = dt.getHours().toString().padStart(2, '0');
var mm = dt.getMinutes().toString().padStart(2, '0');
var ss = dt.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
}
}
使用 ES6 中的字符串新方法 String.prototype.padStart(maxLength, fillString=’’) 或 String.prototype.padEnd(maxLength, fillString=’’)来填充字符串;
全局过滤器
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
// 定义一个全局过滤器
Vue.filter('dataFormat', function (input, pattern = '') {
var dt = new Date(input);
// 获取年月日
var y = dt.getFullYear();
var m = (dt.getMonth() + 1).toString().padStart(2, '0');
var d = dt.getDate().toString().padStart(2, '0');
// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日
// 否则,就返回 年-月-日 时:分:秒
if (pattern.toLowerCase() === 'yyyy-mm-dd') {
return `${y}-${m}-${d}`;
} else {
// 获取时分秒
var hh = dt.getHours().toString().padStart(2, '0');
var mm = dt.getMinutes().toString().padStart(2, '0');
var ss = dt.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
});
注意:当有局部和全局两个名称相同的过滤器时候,会以就近原则进行调用,即:局部过滤器优先于全局过滤器被调用!
键盘修饰符以及自定义键盘修饰符
- 通过
Vue.config.keyCodes.名称 = 按键值
来自定义案件修饰符的别名:
1
Vue.config.keyCodes.f2 = 113;
- 使用自定义的按键修饰符:
1
<input type="text" v-model="name" @keyup.f2="add">
自定义指令
使用 Vue.directive()定义全局的指令,比如 v-focus
其中 参数 1:指令的名称,定义时不需要加 v-的前缀
使用的时候必须在指令名称前面加上 v-前缀来调用
参数 2:是一个对象,这个对象上有一些指令相关的函数,这些函数可以在特定的阶段执行相关的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.dirctive('focus',{
bind:function(el){
//每当指令绑定到元素上的时候会立即执行这个bind函数,只执行一次
//每个函数中的第一个参数永远是el表示被绑定的指令的那个元素,是原生的js对象
//每当指令绑定到元素上的时候会立即执行这个bind函数,只执行一次
// 和样式相关的操作,一般都可以在bind执行
},
inserted(el){
el.focus();
// 和js行为相关的操作,最好在inserted中执行,防止js行为不生效
},
updated(el){
//当Vnode更新时,会执行updated,可能会触发多次
}
}
私有指令的定义
1
2
3
4
5
6
dirctives:{
//自定义指令的简写形式,等同于定义了 bind 和 update 两个钩子函数
'fontsize':function (el,binding){
el.style.fontSize=binding.value
}
}
- 自定义指令的使用方式:
1
<input type="text" v-model="searchName" v-focus v-color="'red'" v-font-weight="900">
实现筛选的方式显示过滤-排序结果:
- 筛选框绑定到 VM 实例中的
searchName
属性:
1
2
3
4
<hr> 输入筛选名称:
<input type="text" v-model="searchName">
- 在使用
v-for
指令循环每一行数据的时候,不再直接 item in list
,而是 in
一个 过滤的 methods 方法,同时,把过滤条件searchName
传递进去:
search
过滤方法中,使用 数组的 filter
方法进行过滤:
1
2
3
4
5
6
7
8
9
10
search(name) {
return this.list.filter(x => {
return x.name.indexOf(name) != -1;
});
}
JSONP 的实现原理
- 由于浏览器安全限制,不允许 AXAJ 访问协议不同、域名不同、端口号不同——不符合同源策略的。
- 可以通过动态创建 script 标签的形式,把 script 标签的 src 属性指向数据接口的地址。因为 script 标签不存在跨域限制,这种数据获取方式称之为 JSONP
具体实现过程
先在客户端定义一个回调方法,预定义对数据的操作;
再把这个回调方法的名称通过 URL 传参的形式提交到服务器的数据接口;
服务器数据接口组织好要发送给客户端的数据,再拿客户端传递过来的回调方法名称拼接出一个调用这个方法的字符串,发送给客户端解析执行;
客户端拿到服务器的返回的字符串之后,当作 script 脚本执行。
Node.js 实现一个 JSONP 的请求例子
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
const http = require('http');
// 导入解析 URL 地址的核心模块
const urlModule = require('url');
const server = http.createServer();
// 监听 服务器的 request 请求事件,处理每个请求
server.on('request', (req, res) => {
const url = req.url;
// 解析客户端请求的URL地址
var info = urlModule.parse(url, true);
// 如果请求的 URL 地址是 /getjsonp ,则表示要获取JSONP类型的数据
if (info.pathname === '/getjsonp') {
// 获取客户端指定的回调函数的名称
var cbName = info.query.callback;
// 手动拼接要返回给客户端的数据对象
var data = {
name: 'zs',
age: 22,
gender: '男',
hobby: ['吃饭', '睡觉', '运动']
}
// 拼接出一个方法的调用,在调用这个方法的时候,把要发送给客户端的数据,序列化为字符串,作为参数传递给这个调用的方法:
var result = `${cbName}(${JSON.stringify(data)})`;
// 将拼接好的方法的调用,返回给客户端去解析执行
res.end(result);
} else {
res.end('404');
}
});
server.listen(3000, () => {
console.log('server running at =http://127.0.0.1:3000');
});
Vue 中的动画
使用过渡类名
- HTML 结构:
1
2
3
4
5
6
7
<div id="app">
<input type="button" value="动起来" @click="myAnimate">
<!-- 使用 transition 将需要过渡的元素包裹起来 -->
<transition name="fade">
<div v-show="isshow">动画哦</div>
</transition>
</div>
- VM 实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
isshow: false
},
methods: {
myAnimate() {
this.isshow = !this.isshow;
}
}
});
- 定义两组类样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 定义进入和离开时候的过渡状态 */
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
position: absolute;
}
/* 定义进入过渡的开始状态 和 离开过渡的结束状态 */
.fade-enter,
.fade-leave-to {
opacity: 0;
transform: translateX(100px);
}
使用第三方 CSS 动画库
- 导入动画类库:
1
<link rel="stylesheet" type="text/css" href="./lib/animate.css">
- 定义 transition 及属性:
1
2
3
4
5
6
<transition
enter-active-class="fadeInRight"
leave-active-class="fadeOutRight"
:duration="{ enter: 500, leave: 800 }">
<div class="animated" v-show="isshow">动画哦</div>
</transition>
使用动画钩子函数
- 定义 transition 组件以及三个钩子函数:
1
2
3
4
5
6
7
8
9
<div id="app">
<input type="button" value="切换动画" @click="isshow = !isshow">
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter">
<div v-if="isshow" class="show">OK</div>
</transition>
</div>
- 定义三个 methods 钩子方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
methods: {
beforeEnter(el) { // 动画进入之前的回调
el.style.transform = 'translateX(500px)';
},
enter(el, done) { // 动画进入完成时候的回调
el.offsetWidth;
el.style.transform = 'translateX(0px)';
done();
},
afterEnter(el) { // 动画进入完成之后的回调
this.isshow = !this.isshow;
}
}
- 定义动画过渡时长和样式:
1
2
3
.show{
transition: all 0.4s ease;
}
v-for 的列表过渡
- 定义过渡样式:
1
2
3
4
5
6
7
8
9
10
11
12
<style>
.list-enter,
.list-leave-to {
opacity: 0;
transform: translateY(10px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
</style>
- 定义 DOM 结构,其中,需要使用 transition-group 组件把 v-for 循环的列表包裹起来:
- 定义 VM 中的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
txt: '',
list: [1, 2, 3, 4]
},
methods: {
add() {
this.list.push(this.txt);
this.txt = '';
}
}
});
列表的排序过渡
<transition-group>
组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move
特性,它会在元素的改变定位的过程中应用。
v-move
和 v-leave-active
结合使用,能够让列表的过渡更加平缓柔和:
1
2
3
4
5
6
.v-move{
transition: all 0.8s ease;
}
.v-leave-active{
position: absolute;
}
定义 Vue 组件
什么是组件: 组件的出现,就是为了拆分 Vue 实例的代码量的,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功能,就可以去调用对应的组件即可;
组件化和模块化的不同:
- 模块化: 是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一;
- 组件化: 是从 UI 界面的角度进行划分的;前端的组件化,方便 UI 组件的重用;
全局组件定义的三种方式
- 使用 Vue.extend 配合 Vue.component 方法:
1
2
3
4
var login = Vue.extend({
template: '<h1>登录</h1>'
});
Vue.component('login', login);
- 直接使用 Vue.component 方法:
1
2
3
Vue.component('register', {
template: '<h1>注册</h1>'
});
- 将模板字符串,定义到 script 标签种:
1
2
3
<script id="tmpl" type="x-template">
</script>
同时,需要使用 Vue.component 来定义组件:
1
2
3
Vue.component('account', {
template: '#tmpl'
});
注意: 组件中的 DOM 结构,有且只能有唯一的根元素(Root Element)来进行包裹!
组件中展示数据和响应事件
- 在组件中,
data
需要被定义为一个方法,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component('account', {
template: '#tmpl',
data() {
return {
msg: '大家好!'
}
},
methods:{
login(){
alert('点击了登录按钮');
}
}
});
- 在子组件中,如果将模板字符串,定义到了 script 标签中,那么,要访问子组件身上的
data
属性中的值,需要使用this
来访问;
- 组件可以有自己的 data 数据
- 组件的 data 和 实例的 data 有点不一样,实例中的 data 可以为一个对象,但是 组件中的 data 必须是一个方法
- 组件中的 data 除了必须为一个方法之外,这个方法内部,还必须返回一个对象才行;
- 组件中 的 data 数据,使用方式,和实例中的 data 使用方式完全一样!!!
组件切换
vue 提供了 component,来展示对应的名称组件
component 是一个占位符, :is 属性,可以用来指定要展示的组件的名称
1
2
3
4
5
<div id="app">
<a href="" @click.prevent="componentId='login'">登录</a>
<a href="" @click.prevent="componentId='register'">注册</a>
<component :is="componentId"></component>
</div>
当前学习了几个 Vue 提供的标签:
component, template, transition, transitionGroup
父组件向子组件传值
- 组件实例定义方式,注意:一定要使用
props
属性来定义父组件传递过来的数据
- 使用
v-bind
或简化指令,将数据传递到子组件中:
1
2
3
<div id="app">
<son :finfo="msg"></son>
</div>
子组件向父组件传值
- 原理:父组件将方法的引用,传递到子组件内部,子组件在内部调用父组件传递过来的方法,同时把要发送给父组件的数据,在调用方法的时候当作参数传递进去;
- 父组件将方法的引用传递给子组件,其中,
getMsg
是父组件中methods
中定义的方法名称,func
是子组件调用传递过来方法时候的方法名称
1
<son @func="getMsg"></son>
- 子组件内部通过
this.$emit('方法名', 要传递的数据)
方式,来调用父组件中的方法,同时把数据传递给父组件使用
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
<div id="app">
<!-- 引用父组件 -->
<son @func="getMsg"></son>
<!-- 组件模板定义 -->
<script type="x-template" id="son">
<div>
<input type="button" value="向父组件传值" @click="sendMsg" />
</div>
</script>
</div>
<script>
// 子组件的定义方式
Vue.component('son', {
template: '#son', // 组件模板Id
methods: {
sendMsg() { // 按钮的点击事件
this.$emit('func', 'OK'); // 调用父组件传递过来的方法,同时把数据传递出去
}
}
});
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {},
methods: {
getMsg(val){ // 子组件中,通过 this.$emit() 实际调用的方法,在此进行定义
alert(val);
}
}
});
</script>
vue-router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const router = new VueRouter({
routes: [
{
path: "/",
redirect: "/home",
meta: {
title: "首页"
}
},
],
mode: "history",
linkActiveClass: "active"
});
// 前置守卫
router.beforeEach((to, from, next) => {
// 从from到to
document.title = to.matched[0].meta.title || "vuebox";
next();
});
// 后置钩子
router.afterEach((to, from) => {
console.log("----------");
});
keep-alive
activated和deactivated只有该组件使用了keep-alive时才是有效的。
组件内路由,beforeRouteLeave(to,from,next){}
include
和exclude
插槽slot
vue2.x
1
2
3
<div slot="item-icon">
<span>前置图标</span>
</div>
vue3.x
1
2
3
4
5
<template v-slot:pre-icon>
<span>
前置图标
</span>
</template>
1
2
this.$router.push('/home/'+123)
this.$router.push({path:'/home',query:{id:123}})
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+vue学习与总结 - AIGISSS vue学习与总结
本文最后更新于:1 年前
插值表达式
- v-cloak
- v-text
- v-html
使用 v-cloak 能够解决 插值表达式闪烁的问题,[v-cloak] {display: none;}
。默认 v-text 是没有闪烁问题的,v-text 会覆盖元素中原本的内容,但是插值表达式 只会替换自己的这个占位符,不会把整个元素的内容清空
v-bind(缩写:)
v-on(缩写@)
v-model 只能用于表单元素
v-for
v-if
v-show
一般来说,v-if 有更高的切换消耗而 v-show 有更高的初始渲染消耗。因此,如果需要频繁切换,v-show 较好,如果运行时条件不太可能改变 v-if 较好
事件修饰符
- .stop 阻止冒泡
- .prevent 阻止默认事件
- .capture 添加事件侦听器时使用事件捕获模式
- .self 只当事件在该元素本身(比如不是子元素)触发时触发回调
- .once 事件只触发一次
在 Vue 中使用样式
使用 class 样式
- 数组
1
<h1 :class="['red', 'thin']">这是一个邪恶的H1</h1>
- 数组中使用三元表达式
1
<h1 :class="['red', 'thin', isactive?'active':'']">这是一个邪恶的H1</h1>
- 数组中嵌套对象
1
<h1 :class="['red', 'thin', {'active': isactive}]">这是一个邪恶的H1</h1>
- 直接使用对象
1
<h1 :class="{red:true, italic:true, active:true, thin:true}">这是一个邪恶的H1</h1>
使用内联样式
- 直接在元素上通过
:style
的形式,书写样式对象
1
<h1 :style="{color: 'red', 'font-size': '40px'}">这是一个善良的H1</h1>
- 将样式对象,定义到
data
中,并直接引用到 :style
中
- 在 data 上定义样式:
1
2
3
data: {
h1StyleObj: { color: 'red', 'font-size': '40px', 'font-weight': '200' }
}
- 在元素中,通过属性绑定的形式,将样式对象应用到元素中:
1
<h1 :style="h1StyleObj">这是一个善良的H1</h1>
- 在
:style
中通过数组,引用多个 data
上的样式对象
- 在 data 上定义样式:
1
2
3
4
data: {
h1StyleObj: { color: 'red', 'font-size': '40px', 'font-weight': '200' },
h1StyleObj2: { fontStyle: 'italic' }
}
- 在元素中,通过属性绑定的形式,将样式对象应用到元素中:
1
<h1 :style="[h1StyleObj, h1StyleObj2]">这是一个善良的H1</h1>
Vue 指令之v-if
和v-show
一般来说,v-if 有更高的切换消耗而 v-show 有更高的初始渲染消耗。因此,如果需要频繁切换 v-show 较好,如果在运行时条件不大可能改变 v-if 较好。
过滤器
概念:Vue.js 允许你自定义过滤器,可被用作一些常见的文本格式化。过滤器可以用在两个地方:mustache 插值和 v-bind 表达式。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符指示;
私有过滤器
- HTML 元素:
- 私有
filters
定义方式:
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
filters: { // 私有局部过滤器,只能在 当前 VM 对象所控制的 View 区域进行使用
dataFormat(input, pattern = "") { // 在参数列表中 通过 pattern="" 来指定形参默认值,防止报错
var dt = new Date(input);
// 获取年月日
var y = dt.getFullYear();
var m = (dt.getMonth() + 1).toString().padStart(2, '0');
var d = dt.getDate().toString().padStart(2, '0');
// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日
// 否则,就返回 年-月-日 时:分:秒
if (pattern.toLowerCase() === 'yyyy-mm-dd') {
return `${y}-${m}-${d}`;
} else {
// 获取时分秒
var hh = dt.getHours().toString().padStart(2, '0');
var mm = dt.getMinutes().toString().padStart(2, '0');
var ss = dt.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
}
}
使用 ES6 中的字符串新方法 String.prototype.padStart(maxLength, fillString=’’) 或 String.prototype.padEnd(maxLength, fillString=’’)来填充字符串;
全局过滤器
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
// 定义一个全局过滤器
Vue.filter('dataFormat', function (input, pattern = '') {
var dt = new Date(input);
// 获取年月日
var y = dt.getFullYear();
var m = (dt.getMonth() + 1).toString().padStart(2, '0');
var d = dt.getDate().toString().padStart(2, '0');
// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日
// 否则,就返回 年-月-日 时:分:秒
if (pattern.toLowerCase() === 'yyyy-mm-dd') {
return `${y}-${m}-${d}`;
} else {
// 获取时分秒
var hh = dt.getHours().toString().padStart(2, '0');
var mm = dt.getMinutes().toString().padStart(2, '0');
var ss = dt.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
});
注意:当有局部和全局两个名称相同的过滤器时候,会以就近原则进行调用,即:局部过滤器优先于全局过滤器被调用!
键盘修饰符以及自定义键盘修饰符
- 通过
Vue.config.keyCodes.名称 = 按键值
来自定义案件修饰符的别名:
1
Vue.config.keyCodes.f2 = 113;
- 使用自定义的按键修饰符:
1
<input type="text" v-model="name" @keyup.f2="add">
自定义指令
使用 Vue.directive()定义全局的指令,比如 v-focus
其中 参数 1:指令的名称,定义时不需要加 v-的前缀
使用的时候必须在指令名称前面加上 v-前缀来调用
参数 2:是一个对象,这个对象上有一些指令相关的函数,这些函数可以在特定的阶段执行相关的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.dirctive('focus',{
bind:function(el){
//每当指令绑定到元素上的时候会立即执行这个bind函数,只执行一次
//每个函数中的第一个参数永远是el表示被绑定的指令的那个元素,是原生的js对象
//每当指令绑定到元素上的时候会立即执行这个bind函数,只执行一次
// 和样式相关的操作,一般都可以在bind执行
},
inserted(el){
el.focus();
// 和js行为相关的操作,最好在inserted中执行,防止js行为不生效
},
updated(el){
//当Vnode更新时,会执行updated,可能会触发多次
}
}
私有指令的定义
1
2
3
4
5
6
dirctives:{
//自定义指令的简写形式,等同于定义了 bind 和 update 两个钩子函数
'fontsize':function (el,binding){
el.style.fontSize=binding.value
}
}
- 自定义指令的使用方式:
1
<input type="text" v-model="searchName" v-focus v-color="'red'" v-font-weight="900">
实现筛选的方式显示过滤-排序结果:
- 筛选框绑定到 VM 实例中的
searchName
属性:
1
2
3
4
<hr> 输入筛选名称:
<input type="text" v-model="searchName">
- 在使用
v-for
指令循环每一行数据的时候,不再直接 item in list
,而是 in
一个 过滤的 methods 方法,同时,把过滤条件searchName
传递进去:
search
过滤方法中,使用 数组的 filter
方法进行过滤:
1
2
3
4
5
6
7
8
9
10
search(name) {
return this.list.filter(x => {
return x.name.indexOf(name) != -1;
});
}
JSONP 的实现原理
- 由于浏览器安全限制,不允许 AXAJ 访问协议不同、域名不同、端口号不同——不符合同源策略的。
- 可以通过动态创建 script 标签的形式,把 script 标签的 src 属性指向数据接口的地址。因为 script 标签不存在跨域限制,这种数据获取方式称之为 JSONP
具体实现过程
先在客户端定义一个回调方法,预定义对数据的操作;
再把这个回调方法的名称通过 URL 传参的形式提交到服务器的数据接口;
服务器数据接口组织好要发送给客户端的数据,再拿客户端传递过来的回调方法名称拼接出一个调用这个方法的字符串,发送给客户端解析执行;
客户端拿到服务器的返回的字符串之后,当作 script 脚本执行。
Node.js 实现一个 JSONP 的请求例子
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
const http = require('http');
// 导入解析 URL 地址的核心模块
const urlModule = require('url');
const server = http.createServer();
// 监听 服务器的 request 请求事件,处理每个请求
server.on('request', (req, res) => {
const url = req.url;
// 解析客户端请求的URL地址
var info = urlModule.parse(url, true);
// 如果请求的 URL 地址是 /getjsonp ,则表示要获取JSONP类型的数据
if (info.pathname === '/getjsonp') {
// 获取客户端指定的回调函数的名称
var cbName = info.query.callback;
// 手动拼接要返回给客户端的数据对象
var data = {
name: 'zs',
age: 22,
gender: '男',
hobby: ['吃饭', '睡觉', '运动']
}
// 拼接出一个方法的调用,在调用这个方法的时候,把要发送给客户端的数据,序列化为字符串,作为参数传递给这个调用的方法:
var result = `${cbName}(${JSON.stringify(data)})`;
// 将拼接好的方法的调用,返回给客户端去解析执行
res.end(result);
} else {
res.end('404');
}
});
server.listen(3000, () => {
console.log('server running at =http://127.0.0.1:3000');
});
Vue 中的动画
使用过渡类名
- HTML 结构:
1
2
3
4
5
6
7
<div id="app">
<input type="button" value="动起来" @click="myAnimate">
<!-- 使用 transition 将需要过渡的元素包裹起来 -->
<transition name="fade">
<div v-show="isshow">动画哦</div>
</transition>
</div>
- VM 实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
isshow: false
},
methods: {
myAnimate() {
this.isshow = !this.isshow;
}
}
});
- 定义两组类样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 定义进入和离开时候的过渡状态 */
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
position: absolute;
}
/* 定义进入过渡的开始状态 和 离开过渡的结束状态 */
.fade-enter,
.fade-leave-to {
opacity: 0;
transform: translateX(100px);
}
使用第三方 CSS 动画库
- 导入动画类库:
1
<link rel="stylesheet" type="text/css" href="./lib/animate.css">
- 定义 transition 及属性:
1
2
3
4
5
6
<transition
enter-active-class="fadeInRight"
leave-active-class="fadeOutRight"
:duration="{ enter: 500, leave: 800 }">
<div class="animated" v-show="isshow">动画哦</div>
</transition>
使用动画钩子函数
- 定义 transition 组件以及三个钩子函数:
1
2
3
4
5
6
7
8
9
<div id="app">
<input type="button" value="切换动画" @click="isshow = !isshow">
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter">
<div v-if="isshow" class="show">OK</div>
</transition>
</div>
- 定义三个 methods 钩子方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
methods: {
beforeEnter(el) { // 动画进入之前的回调
el.style.transform = 'translateX(500px)';
},
enter(el, done) { // 动画进入完成时候的回调
el.offsetWidth;
el.style.transform = 'translateX(0px)';
done();
},
afterEnter(el) { // 动画进入完成之后的回调
this.isshow = !this.isshow;
}
}
- 定义动画过渡时长和样式:
1
2
3
.show{
transition: all 0.4s ease;
}
v-for 的列表过渡
- 定义过渡样式:
1
2
3
4
5
6
7
8
9
10
11
12
<style>
.list-enter,
.list-leave-to {
opacity: 0;
transform: translateY(10px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
</style>
- 定义 DOM 结构,其中,需要使用 transition-group 组件把 v-for 循环的列表包裹起来:
- 定义 VM 中的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
txt: '',
list: [1, 2, 3, 4]
},
methods: {
add() {
this.list.push(this.txt);
this.txt = '';
}
}
});
列表的排序过渡
<transition-group>
组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move
特性,它会在元素的改变定位的过程中应用。
v-move
和 v-leave-active
结合使用,能够让列表的过渡更加平缓柔和:
1
2
3
4
5
6
.v-move{
transition: all 0.8s ease;
}
.v-leave-active{
position: absolute;
}
定义 Vue 组件
什么是组件: 组件的出现,就是为了拆分 Vue 实例的代码量的,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功能,就可以去调用对应的组件即可;
组件化和模块化的不同:
- 模块化: 是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一;
- 组件化: 是从 UI 界面的角度进行划分的;前端的组件化,方便 UI 组件的重用;
全局组件定义的三种方式
- 使用 Vue.extend 配合 Vue.component 方法:
1
2
3
4
var login = Vue.extend({
template: '<h1>登录</h1>'
});
Vue.component('login', login);
- 直接使用 Vue.component 方法:
1
2
3
Vue.component('register', {
template: '<h1>注册</h1>'
});
- 将模板字符串,定义到 script 标签种:
1
2
3
<script id="tmpl" type="x-template">
</script>
同时,需要使用 Vue.component 来定义组件:
1
2
3
Vue.component('account', {
template: '#tmpl'
});
注意: 组件中的 DOM 结构,有且只能有唯一的根元素(Root Element)来进行包裹!
组件中展示数据和响应事件
- 在组件中,
data
需要被定义为一个方法,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component('account', {
template: '#tmpl',
data() {
return {
msg: '大家好!'
}
},
methods:{
login(){
alert('点击了登录按钮');
}
}
});
- 在子组件中,如果将模板字符串,定义到了 script 标签中,那么,要访问子组件身上的
data
属性中的值,需要使用this
来访问;
- 组件可以有自己的 data 数据
- 组件的 data 和 实例的 data 有点不一样,实例中的 data 可以为一个对象,但是 组件中的 data 必须是一个方法
- 组件中的 data 除了必须为一个方法之外,这个方法内部,还必须返回一个对象才行;
- 组件中 的 data 数据,使用方式,和实例中的 data 使用方式完全一样!!!
组件切换
vue 提供了 component,来展示对应的名称组件
component 是一个占位符, :is 属性,可以用来指定要展示的组件的名称
1
2
3
4
5
<div id="app">
<a href="" @click.prevent="componentId='login'">登录</a>
<a href="" @click.prevent="componentId='register'">注册</a>
<component :is="componentId"></component>
</div>
当前学习了几个 Vue 提供的标签:
component, template, transition, transitionGroup
父组件向子组件传值
- 组件实例定义方式,注意:一定要使用
props
属性来定义父组件传递过来的数据
- 使用
v-bind
或简化指令,将数据传递到子组件中:
1
2
3
<div id="app">
<son :finfo="msg"></son>
</div>
子组件向父组件传值
- 原理:父组件将方法的引用,传递到子组件内部,子组件在内部调用父组件传递过来的方法,同时把要发送给父组件的数据,在调用方法的时候当作参数传递进去;
- 父组件将方法的引用传递给子组件,其中,
getMsg
是父组件中methods
中定义的方法名称,func
是子组件调用传递过来方法时候的方法名称
1
<son @func="getMsg"></son>
- 子组件内部通过
this.$emit('方法名', 要传递的数据)
方式,来调用父组件中的方法,同时把数据传递给父组件使用
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
<div id="app">
<!-- 引用父组件 -->
<son @func="getMsg"></son>
<!-- 组件模板定义 -->
<script type="x-template" id="son">
<div>
<input type="button" value="向父组件传值" @click="sendMsg" />
</div>
</script>
</div>
<script>
// 子组件的定义方式
Vue.component('son', {
template: '#son', // 组件模板Id
methods: {
sendMsg() { // 按钮的点击事件
this.$emit('func', 'OK'); // 调用父组件传递过来的方法,同时把数据传递出去
}
}
});
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {},
methods: {
getMsg(val){ // 子组件中,通过 this.$emit() 实际调用的方法,在此进行定义
alert(val);
}
}
});
</script>
vue-router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const router = new VueRouter({
routes: [
{
path: "/",
redirect: "/home",
meta: {
title: "首页"
}
},
],
mode: "history",
linkActiveClass: "active"
});
// 前置守卫
router.beforeEach((to, from, next) => {
// 从from到to
document.title = to.matched[0].meta.title || "vuebox";
next();
});
// 后置钩子
router.afterEach((to, from) => {
console.log("----------");
});
keep-alive
activated和deactivated只有该组件使用了keep-alive时才是有效的。
组件内路由,beforeRouteLeave(to,from,next){}
include
和exclude
插槽slot
vue2.x
1
2
3
<div slot="item-icon">
<span>前置图标</span>
</div>
vue3.x
1
2
3
4
5
<template v-slot:pre-icon>
<span>
前置图标
</span>
</template>
1
2
this.$router.push('/home/'+123)
this.$router.push({path:'/home',query:{id:123}})
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/423dd834.html b/posts/423dd834.html
index 3303f5dd..12d6ba58 100644
--- a/posts/423dd834.html
+++ b/posts/423dd834.html
@@ -1 +1 @@
-着色器之书01 - AIGISSS 着色器之书01
本文最后更新于:1 个月前
关于这本书
引言
上面两幅图是由不同的方式制成的。第一张是梵高一层一层徒手画出来的,需要花费些时间。第二张则是用 4 个像素矩阵分秒钟生成的:一个青色,一个品红,一个黄色,和一个黑色矩阵。关键的区别在于第二张图是用非序列方式实现的(即不是一步一步实现,而是多个同时进行)。
这本书是关于这个革命性的计算机技术,片段着色器(fragment shaders),它将数字生成的图像提到了新的层次。你可以把它看做当年的古腾堡印刷术。

Fragment shaders(片段着色器)可以让你控制像素在屏幕上的快速渲染。这就是它在各种场合被广泛使用的原因,从手机的视频滤镜到酷炫的的3D视频游戏。

在接下来的章节你会发现这项技术是多么难以置信地快速和强大,还有如何将它应用到专业的和个人的作品中。
这本书是为谁而写的?
这本书是写给有代码经验和线性代数、三角学的基本知识的创意编程者、游戏开发者和工程师的,还有那些想要提升他们的作品的图像质量到一个令人激动的新层次的人。(如果你想要学习编程,我强烈推荐你先学习Processing,等你玩起来processing,再回来看这个)。
这本书会教你如何使用 shaders(着色器)并把它整合进你的项目里,以提升作品的表现力和图形质量。因为GLSL(OpenGL的绘制语言)的shaders 在很多平台都可以编译和运行,你将可以把在这里学的运用到任何使用OpenGL, OpenGL ES 和 WebGL 的环境中。也就是说,你将可以把学到的知识应用到Processing,openFrameworks,Cinder,Three.js和iOS/Android游戏中。
这本书包含哪些内容?
这本书专门关于 GLSL pixel shaders。首先我们会给出shaders的定义;然后我们会学习如何制作程序里的形状,图案,材质,和与之相关的动画。你将会学到基础的着色语言并把它们应用到有用的情景中,比如:图像处理(图像运算,矩阵卷积,模糊,颜色滤镜,查找表及其他效果)和模拟(Conway 的生命游戏,Gray-Scott 反应扩散,水波,水彩效果,Voronoi 细胞等等)。到书的最后我们将看到一系列基于光线跟踪(Ray Marching)的进阶技术。
每章都会有可以玩的交互的例子。当你改动代码的时候,你会立刻看到这些变化。一些概念可能会晦涩难懂,而这些可交互的例子会对你学习这些材料非常有益。你越快把这些代码付诸实践,你学习的过程就会越容易。
这本书里不包括的内容有:
这不是一本 openGL 或 webGL 的书。OpenGL/webGL 是一个比GLSL 或 fragment shaders 更大的主题。如果你想要学习 openGL/webGL 推荐看: OpenGL Introduction, the 8th edition of the OpenGL Programming Guide (也被叫做红宝书) 或 WebGL: Up and Running
。
这不是一本数学书。虽然我们会涉及到很多关于线代和三角学的算法和技术,但我们不会详细解释它。关于数学的问题我推荐手边备一本:3rd Edition of Mathematics for 3D Game Programming and computer Graphics 或 2nd Edition of Essential Mathematics for Games and Interactive Applications。
开始学习需要什么准备?
没什么。如果你有可以运行 WebGL 的浏览器(像Chrome,Firefox或Safari)和网络,点击页面底端的“下一章”按钮就可以开始了。
此外,基于你有的条件或需求你可以:
用github仓库来帮助解决问题和分享代码
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书01 - AIGISSS 着色器之书01
本文最后更新于:1 个月前
关于这本书
引言
上面两幅图是由不同的方式制成的。第一张是梵高一层一层徒手画出来的,需要花费些时间。第二张则是用 4 个像素矩阵分秒钟生成的:一个青色,一个品红,一个黄色,和一个黑色矩阵。关键的区别在于第二张图是用非序列方式实现的(即不是一步一步实现,而是多个同时进行)。
这本书是关于这个革命性的计算机技术,片段着色器(fragment shaders),它将数字生成的图像提到了新的层次。你可以把它看做当年的古腾堡印刷术。

Fragment shaders(片段着色器)可以让你控制像素在屏幕上的快速渲染。这就是它在各种场合被广泛使用的原因,从手机的视频滤镜到酷炫的的3D视频游戏。

在接下来的章节你会发现这项技术是多么难以置信地快速和强大,还有如何将它应用到专业的和个人的作品中。
这本书是为谁而写的?
这本书是写给有代码经验和线性代数、三角学的基本知识的创意编程者、游戏开发者和工程师的,还有那些想要提升他们的作品的图像质量到一个令人激动的新层次的人。(如果你想要学习编程,我强烈推荐你先学习Processing,等你玩起来processing,再回来看这个)。
这本书会教你如何使用 shaders(着色器)并把它整合进你的项目里,以提升作品的表现力和图形质量。因为GLSL(OpenGL的绘制语言)的shaders 在很多平台都可以编译和运行,你将可以把在这里学的运用到任何使用OpenGL, OpenGL ES 和 WebGL 的环境中。也就是说,你将可以把学到的知识应用到Processing,openFrameworks,Cinder,Three.js和iOS/Android游戏中。
这本书包含哪些内容?
这本书专门关于 GLSL pixel shaders。首先我们会给出shaders的定义;然后我们会学习如何制作程序里的形状,图案,材质,和与之相关的动画。你将会学到基础的着色语言并把它们应用到有用的情景中,比如:图像处理(图像运算,矩阵卷积,模糊,颜色滤镜,查找表及其他效果)和模拟(Conway 的生命游戏,Gray-Scott 反应扩散,水波,水彩效果,Voronoi 细胞等等)。到书的最后我们将看到一系列基于光线跟踪(Ray Marching)的进阶技术。
每章都会有可以玩的交互的例子。当你改动代码的时候,你会立刻看到这些变化。一些概念可能会晦涩难懂,而这些可交互的例子会对你学习这些材料非常有益。你越快把这些代码付诸实践,你学习的过程就会越容易。
这本书里不包括的内容有:
这不是一本 openGL 或 webGL 的书。OpenGL/webGL 是一个比GLSL 或 fragment shaders 更大的主题。如果你想要学习 openGL/webGL 推荐看: OpenGL Introduction, the 8th edition of the OpenGL Programming Guide (也被叫做红宝书) 或 WebGL: Up and Running
。
这不是一本数学书。虽然我们会涉及到很多关于线代和三角学的算法和技术,但我们不会详细解释它。关于数学的问题我推荐手边备一本:3rd Edition of Mathematics for 3D Game Programming and computer Graphics 或 2nd Edition of Essential Mathematics for Games and Interactive Applications。
开始学习需要什么准备?
没什么。如果你有可以运行 WebGL 的浏览器(像Chrome,Firefox或Safari)和网络,点击页面底端的“下一章”按钮就可以开始了。
此外,基于你有的条件或需求你可以:
用github仓库来帮助解决问题和分享代码
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/45501c2d.html b/posts/45501c2d.html
index 1decf848..6515f516 100644
--- a/posts/45501c2d.html
+++ b/posts/45501c2d.html
@@ -1,4 +1,4 @@
-着色器之书05 - AIGISSS 着色器之书05
本文最后更新于:1 个月前
算法绘画
造型函数
这一章应该叫做宫城先生的粉刷课(来自电影龙威小子的经典桥段)。之前我们把规范化后的 x,y 坐标映射(map)到了红色和绿色通道。本质上说我们是建造了这样一个函数:输入一个二维向量(x,y),然后返回一个四维向量(r,g,b,a)。但在我们跨维度转换数据之前,我们先从更加…更加简单的开始。我们来建一个只有一维变量的函数。你花越多的时间和精力在这上面,你的 shader 功夫就越厉害。

接下来的代码结构就是我们的基本功。在它之中我们对规范化的 x 坐标(st.x
)进行可视化。有两种途径:一种是用亮度(度量从黑色到白色的渐变过程),另一种是在顶层绘制一条绿色的线(在这种情况下 x 被直接赋值给 y)。不用过分在意绘制函数,我们马上会更加详细地解释它。
简注 :vec3
类型构造器“明白”你想要把一个值赋值到颜色的三个通道里,就像 vec4
明白你想要构建一个四维向量,三维向量加上第四个值(比如颜色的三个值加上透明度)。请参照上面示例的第 19 到 25 行。
这些代码就是你的基本功;遵守和理解它非常重要。你将会一遍又一遍地回到 0.0 到 1.0 这个区间。你将会掌握融合与构建这些代码的艺术。
这些 x 与 y(或亮度)之间一对一的关系称作线性插值(linear interpolation)。(译者注:插值是离散函数逼近的重要方法,利用它可通过函数在有限个点处的取值状况,估算出函数在其他点处的近似值。因为对计算机来说,屏幕像素是离散的而不是连续的,计算机图形学常用插值来填充图像像素之间的空隙。)现在起我们可以用一些数学函数来改造这些代码行。比如说我们可以做一个求 x 的 5 次幂的曲线。
很有趣,对吧?试试看把第 19 行的指数改为不同的值,比如:20.0,2.0,1.0,0.0,0.2 或 0.02。理解值和指数之间的关系非常重要。这些数学函数可以让你灵动地控制你的代码,就像是给数据做针灸一样。
pow()
(求 x 的 y 次幂)是 GLSL 的一个原生函数,GLSL 有很多原生函数。大多数原生函数都是硬件加速的,也就是说如果你正确使用这些函数,你的代码就会跑得更快。
换掉第 19 行的幂函数,试试看exp()
(以自然常数 e 为底的指数函数),log()
(对数函数) 和 sqrt()
(平方根函数)。当你用 Pi 来玩的时候有些方程会变得更有趣。在第 5 行我定义了一个宏,使得每当程序调用 PI
的时候就用 3.14159265359
来替换它。
Step 和 Smoothstep
GLSL 还有一些独特的原生插值函数可以被硬件加速。
step()
插值函数需要输入两个参数。第一个是极限或阈值,第二个是我们想要检测或通过的值。对任何小于阈值的值,返回 0.0
,大于阈值,则返回 1.0
。
试试看改变下述代码中第 20 行的值。
另一个 GLSL 的特殊函数是 smoothstep()
。当给定一个范围的上下限和一个数值,这个函数会在已有的范围内给出插值。前两个参数规定转换的开始和结束点,第三个是给出一个值用来插值。
在之前的例子中,注意第 12 行,我们用到 smoothstep 在 plot()
函数中画了一条绿色的线。这个函数会对给出的 x 轴上的每个值,在特定的 y 值处制造一个凹凸形变。如何做到呢?通过把两个 smoothstep()
连接到一起。来看看下面这个函数,用它替换上面的第 20 行,把它想成是一个垂直切割。背景看起来很像一条线,不是吗?
1
float y = smoothstep(0.2,0.5,st.x) - smoothstep(0.5,0.8,st.x);
正弦和余弦函数
当你想用数学来制造动效,形态或去混合数值,sin 和 cos 就是你的最佳伙伴。
这两个基础的三角函数是构造圆的极佳工具,就像张小泉的剪刀一样称手。很重要的一点是你需要知道它们是如何运转的,还有如何把它们结合起来。简单来说,当我们给出一个角度(这里采用弧度制),它就会返回半径为一的圆上一个点的 x 坐标(cos)和 y 坐标(sin)。正因为 sin 和 cos 返回的是规范化的值(即值域在 -1 和 1 之间),且如此流畅,这就使得它成为一个极其强大的工具。

尽管描述三角函数和圆的关系是一件蛮困难的事情,上图动画很棒地做到了这一点,视觉化展现了它们之间的关系。
仔细看 sin 曲线。观察 y 值是如何平滑地在 +1 和 -1 之间变化。就像之前章节关于的 time 的例子中,你可以用 sin()
的有节奏的变动给其他东西加动效。如果你是在用浏览器阅读的话你可以改动上述公式,看看曲线会如何变动。(注:不要忘记每行最后要加分号!)
试试下面的小练习,看看会发生什么:
在 sin
里让 x 加上时间(u_time
)。让 sin 曲线随 x 轴动起来。
在 sin
里用 PI
乘以 x。注意 sin 曲线上下波动的两部分如何收缩了,现在 sin 曲线每两个整数循环一次。
在 sin
里用时间( u_time
)乘以 x。观察各阶段的循环如何变得越来越频繁。注意 u_time 可能已经变得非常大,使得图像难以辨认。
给 sin(x)
(注意不是 sin 里的 x)加 1.0。观察曲线是如何向上移动的,现在值域变成了 0.0 到 2.0。
给 sin(x)
乘以 2.0。观察曲线大小如何增大两倍。
计算 sin(x)
的绝对值(abs()
)。现在它看起来就像一个弹力球的轨迹。
只选取 sin(x)
的小数部分(fract()
)。
其他有用的函数
最后一个练习中我们介绍了一些新函数。现在我们来一个一个试一遍。依次取消注释下列各行,理解这些函数,观察它们是如何运作的。你一定在奇怪……为什么要这么做呢?Google 一下“generative art”(生成艺术)你就知道了。要知道这些函数就是我们的栅栏。我们现在控制的是它在一维中的移动,上上下下。很快,我们就可以尝试二维、三维甚至四维了!

着色器之书05 - AIGISSS 着色器之书05
本文最后更新于:1 个月前
算法绘画
造型函数
这一章应该叫做宫城先生的粉刷课(来自电影龙威小子的经典桥段)。之前我们把规范化后的 x,y 坐标映射(map)到了红色和绿色通道。本质上说我们是建造了这样一个函数:输入一个二维向量(x,y),然后返回一个四维向量(r,g,b,a)。但在我们跨维度转换数据之前,我们先从更加…更加简单的开始。我们来建一个只有一维变量的函数。你花越多的时间和精力在这上面,你的 shader 功夫就越厉害。

接下来的代码结构就是我们的基本功。在它之中我们对规范化的 x 坐标(st.x
)进行可视化。有两种途径:一种是用亮度(度量从黑色到白色的渐变过程),另一种是在顶层绘制一条绿色的线(在这种情况下 x 被直接赋值给 y)。不用过分在意绘制函数,我们马上会更加详细地解释它。
简注 :vec3
类型构造器“明白”你想要把一个值赋值到颜色的三个通道里,就像 vec4
明白你想要构建一个四维向量,三维向量加上第四个值(比如颜色的三个值加上透明度)。请参照上面示例的第 19 到 25 行。
这些代码就是你的基本功;遵守和理解它非常重要。你将会一遍又一遍地回到 0.0 到 1.0 这个区间。你将会掌握融合与构建这些代码的艺术。
这些 x 与 y(或亮度)之间一对一的关系称作线性插值(linear interpolation)。(译者注:插值是离散函数逼近的重要方法,利用它可通过函数在有限个点处的取值状况,估算出函数在其他点处的近似值。因为对计算机来说,屏幕像素是离散的而不是连续的,计算机图形学常用插值来填充图像像素之间的空隙。)现在起我们可以用一些数学函数来改造这些代码行。比如说我们可以做一个求 x 的 5 次幂的曲线。
很有趣,对吧?试试看把第 19 行的指数改为不同的值,比如:20.0,2.0,1.0,0.0,0.2 或 0.02。理解值和指数之间的关系非常重要。这些数学函数可以让你灵动地控制你的代码,就像是给数据做针灸一样。
pow()
(求 x 的 y 次幂)是 GLSL 的一个原生函数,GLSL 有很多原生函数。大多数原生函数都是硬件加速的,也就是说如果你正确使用这些函数,你的代码就会跑得更快。
换掉第 19 行的幂函数,试试看exp()
(以自然常数 e 为底的指数函数),log()
(对数函数) 和 sqrt()
(平方根函数)。当你用 Pi 来玩的时候有些方程会变得更有趣。在第 5 行我定义了一个宏,使得每当程序调用 PI
的时候就用 3.14159265359
来替换它。
Step 和 Smoothstep
GLSL 还有一些独特的原生插值函数可以被硬件加速。
step()
插值函数需要输入两个参数。第一个是极限或阈值,第二个是我们想要检测或通过的值。对任何小于阈值的值,返回 0.0
,大于阈值,则返回 1.0
。
试试看改变下述代码中第 20 行的值。
另一个 GLSL 的特殊函数是 smoothstep()
。当给定一个范围的上下限和一个数值,这个函数会在已有的范围内给出插值。前两个参数规定转换的开始和结束点,第三个是给出一个值用来插值。
在之前的例子中,注意第 12 行,我们用到 smoothstep 在 plot()
函数中画了一条绿色的线。这个函数会对给出的 x 轴上的每个值,在特定的 y 值处制造一个凹凸形变。如何做到呢?通过把两个 smoothstep()
连接到一起。来看看下面这个函数,用它替换上面的第 20 行,把它想成是一个垂直切割。背景看起来很像一条线,不是吗?
1
float y = smoothstep(0.2,0.5,st.x) - smoothstep(0.5,0.8,st.x);
正弦和余弦函数
当你想用数学来制造动效,形态或去混合数值,sin 和 cos 就是你的最佳伙伴。
这两个基础的三角函数是构造圆的极佳工具,就像张小泉的剪刀一样称手。很重要的一点是你需要知道它们是如何运转的,还有如何把它们结合起来。简单来说,当我们给出一个角度(这里采用弧度制),它就会返回半径为一的圆上一个点的 x 坐标(cos)和 y 坐标(sin)。正因为 sin 和 cos 返回的是规范化的值(即值域在 -1 和 1 之间),且如此流畅,这就使得它成为一个极其强大的工具。

尽管描述三角函数和圆的关系是一件蛮困难的事情,上图动画很棒地做到了这一点,视觉化展现了它们之间的关系。
仔细看 sin 曲线。观察 y 值是如何平滑地在 +1 和 -1 之间变化。就像之前章节关于的 time 的例子中,你可以用 sin()
的有节奏的变动给其他东西加动效。如果你是在用浏览器阅读的话你可以改动上述公式,看看曲线会如何变动。(注:不要忘记每行最后要加分号!)
试试下面的小练习,看看会发生什么:
在 sin
里让 x 加上时间(u_time
)。让 sin 曲线随 x 轴动起来。
在 sin
里用 PI
乘以 x。注意 sin 曲线上下波动的两部分如何收缩了,现在 sin 曲线每两个整数循环一次。
在 sin
里用时间( u_time
)乘以 x。观察各阶段的循环如何变得越来越频繁。注意 u_time 可能已经变得非常大,使得图像难以辨认。
给 sin(x)
(注意不是 sin 里的 x)加 1.0。观察曲线是如何向上移动的,现在值域变成了 0.0 到 2.0。
给 sin(x)
乘以 2.0。观察曲线大小如何增大两倍。
计算 sin(x)
的绝对值(abs()
)。现在它看起来就像一个弹力球的轨迹。
只选取 sin(x)
的小数部分(fract()
)。
其他有用的函数
最后一个练习中我们介绍了一些新函数。现在我们来一个一个试一遍。依次取消注释下列各行,理解这些函数,观察它们是如何运作的。你一定在奇怪……为什么要这么做呢?Google 一下“generative art”(生成艺术)你就知道了。要知道这些函数就是我们的栅栏。我们现在控制的是它在一维中的移动,上上下下。很快,我们就可以尝试二维、三维甚至四维了!

Threejs学习和总结前传 - AIGISSS Threejs学习和总结前传
本文最后更新于:1 年前
学习 Three.js 之前要知道的
什么是 WebGL?
WebGL(Web 图形库)是一种 JavaScript API,用于在任何兼容的 Web 浏览器中呈现交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 紧密相符合的 API,可以在 HTML5 <canvas>
元素中使用。WebGL 给我们提供了一系列的图形接口,能够让我们通过 JavaScript 去使用 GPU 来进行浏览器图形渲染的工具。
什么是 Three.js?
Three.js 是一款 webGL 框架,由于其易用性被广泛应用。Three.js 在 WebGL 的 API 接口基础上,又进行的一层封装。Three.js 以简单、直观的方式封装了 3D 图形编程中常用的对象。Three.js 在开发中使用了很多图形引擎的高级技巧,极大地提高了性能。另外,由于内置了很多常用对象和极易上手的工具,Three.js 的功能也非常强大,Three.js 还是完全开源的。
WEBGL 和 Three.js 的关系
WebGL 原生 API 是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js 将入门的门槛降低了一大截,对 WebGL 进行封装,简化我们创建三维动画场景的过程。
用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。
功能概述
Three.js 作为 WebGL 框架中的佼佼者,由于它的易用性和扩展性,使得它能够满足大部分的开发需求,Three.js 的具体功能如下:
- Three.js 掩盖了 3D 渲染的细节:Three.js 将 WebGL 原生 API 的细节抽象化,将 3D 场景拆解为网格、材质和光源(即它内置了图形编程常用的一些对象种类)。
- 面向对象:开发者可以使用上层的 JavaScript 对象,而不是仅仅调用 JavaScript 函数。
- 功能非常丰富:Three.js 除封装了 WebGL 原始 API 之外,Three.js 还包含了许多实用的内置对象,可以方便地应用于游戏开发、动画制作、幻灯片制作、髙分辨率模型和一些特殊的视觉效果制作。
- 速度很快:Three.js 采用了 3D 图形最佳实践来保证在不失可用性的前提下,保持极高的性能。
- 支持交互:WebGL 本身并不提供拾取(Picking)功能(即是否知道鼠标正处于某个物体上)。而 Three.js 则固化了拾取支持,这就使得你可以轻松为你的应用添加交互功能。
- 包含数学库:Three.js 拥有一个强大易用的数学库,你可以在其中进行矩阵、投影和矢量运算。
- 内置文件格式支持:你可以使用流行的 3D 建模软件导出文本格式的文件,然后使用 Three.js 加载,也可以使用 Three.js 自己的 JSON 格式或二进制格式。
- 扩展性很强:为 Three.js 添加新的特性或进行自定义优化是很容易的事情。如果你需要某个特殊的数据结构,那么只需要封装到 Three.js 即可。
- 支持 HTML5 Canvas:Three.js 不但支持 WebGL,而且还支持使用 Canvas2D、Css3D 和 SVG 进行渲染。在未兼容 WebGL 的环境中可以回退到其它的解决方案。
缺点
- 官网文档非常粗糙,对于新手极度不友好。
- 国内的相关资源匮乏。
- Three.js 所有的资料都是以英文格式存在,对国内的朋友来说又提高了门槛。
- Three.js 不是游戏引擎,一些游戏相关的功能没有封装在里面,如果需要相关的功能需要进行二次开发。
Three.js 与其他库的对比
库名 描述 相比 Three.js Babylon.js 是最好的 JavaScript 3D 游戏引擎,它能创建专业级三维游戏。主要以游戏开发和易用性为主 Three.js 比较全面,而 Babylon.js 专注于游戏方面。
Babylon.js 提供了对碰撞检测、场景重力、面向游戏的照相机,Three.js 本身不自带,需要依靠引入插件实现。
对于 WebGL 的封装,双方做得各有千秋,Three.js 浅一些,好处是易于扩展,易于向更底层学习;Babylon.js 深一些,好处是易用,但扩展难度大一些。
Three.js 的发展依靠社区推动,出来的比较早,发展比较成熟,Babylon.js 由微软公司在 2013 推出,文档和社区都比较健全,国内还不怎么火。
PlayCanvas 是一个基于 WebGL 游戏引擎的企业级开源 JavaScript 框架,它有许多的开发工具能帮你快速创建 3D 游戏 PlayCanvas 的优势在于它有云端的在线可视化编辑工具。 PlayCanvas 的扩展性不如 Three.js。 最主要是 PlayCanvas 不完全开源,还商业付费。 Cesium 是国外一个基于 JavaScript 编写的使用 WebGL 的地图引擎,支持 3D、2D、2.5D 形式的地图展示,可以自行绘制图形,高亮区域。 Cesium 是一个地图引擎,专注于 GIS,相关项目推荐使用它,其它项目还是算了。 至于库的扩展,其它的配套插件,以及周边的资源都不及 Three.js。
Three.js 在其库的扩展性,易用性以及功能方面有很好的优势。
Three.js-master 源码目录结构
- build:里面含有 Three.js 构建出来的 JavaScript 文件,可以直接引入使用,并有压缩版;
- docs:Three.js 的官方文档;
- editor:Three.js 的一个网页版的模型编辑器;
- examples:Three.js 的官方案例,如果全都学会,必将成为大神;
- src:这里面放置的全是编译 Three.js 的源文件;
- test:一些官方测试代码,我们一般用不到;
- utils:一些相关插件;
- 其他:开发环境搭建、开发所需要的文件,如果不对 Three.js 进行二次开发,用不到。
使用 Three.js
第一个案例
所有关于Threejs的代码均在https://github.com/Cenergy/webpack-threejs.git
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>第一个Three.js案例</title>
<style>
body {
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
</head>
<body onload="init()">
<script src="https://cdn.bootcss.com/three.js/108/three.js"></script>
<script>
//声明一些全局变量
var renderer, camera, scene, geometry, material, mesh;
//初始化渲染器
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高
document.body.appendChild(renderer.domElement); //添加到dom
}
//初始化场景
function initScene() {
scene = new THREE.Scene(); //实例化场景
}
//初始化相机
function initCamera() {
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
); //实例化相机
camera.position.set(0, 0, 15);
}
//创建模型
function initMesh() {
geometry = new THREE.BoxGeometry(2, 2, 2); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh(geometry, material); //创建网格
scene.add(mesh); //将网格添加到场景
}
//运行动画
function animate() {
requestAnimationFrame(animate); //循环调用函数
mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度
mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度
renderer.render(scene, camera); //渲染界面
}
//初始化函数,页面加载完成是调用
function init() {
initRenderer();
initScene();
initCamera();
initMesh();
animate();
}
</script>
</body>
</html>
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+Threejs学习和总结前传 - AIGISSS Threejs学习和总结前传
本文最后更新于:1 年前
学习 Three.js 之前要知道的
什么是 WebGL?
WebGL(Web 图形库)是一种 JavaScript API,用于在任何兼容的 Web 浏览器中呈现交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 紧密相符合的 API,可以在 HTML5 <canvas>
元素中使用。WebGL 给我们提供了一系列的图形接口,能够让我们通过 JavaScript 去使用 GPU 来进行浏览器图形渲染的工具。
什么是 Three.js?
Three.js 是一款 webGL 框架,由于其易用性被广泛应用。Three.js 在 WebGL 的 API 接口基础上,又进行的一层封装。Three.js 以简单、直观的方式封装了 3D 图形编程中常用的对象。Three.js 在开发中使用了很多图形引擎的高级技巧,极大地提高了性能。另外,由于内置了很多常用对象和极易上手的工具,Three.js 的功能也非常强大,Three.js 还是完全开源的。
WEBGL 和 Three.js 的关系
WebGL 原生 API 是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js 将入门的门槛降低了一大截,对 WebGL 进行封装,简化我们创建三维动画场景的过程。
用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。
功能概述
Three.js 作为 WebGL 框架中的佼佼者,由于它的易用性和扩展性,使得它能够满足大部分的开发需求,Three.js 的具体功能如下:
- Three.js 掩盖了 3D 渲染的细节:Three.js 将 WebGL 原生 API 的细节抽象化,将 3D 场景拆解为网格、材质和光源(即它内置了图形编程常用的一些对象种类)。
- 面向对象:开发者可以使用上层的 JavaScript 对象,而不是仅仅调用 JavaScript 函数。
- 功能非常丰富:Three.js 除封装了 WebGL 原始 API 之外,Three.js 还包含了许多实用的内置对象,可以方便地应用于游戏开发、动画制作、幻灯片制作、髙分辨率模型和一些特殊的视觉效果制作。
- 速度很快:Three.js 采用了 3D 图形最佳实践来保证在不失可用性的前提下,保持极高的性能。
- 支持交互:WebGL 本身并不提供拾取(Picking)功能(即是否知道鼠标正处于某个物体上)。而 Three.js 则固化了拾取支持,这就使得你可以轻松为你的应用添加交互功能。
- 包含数学库:Three.js 拥有一个强大易用的数学库,你可以在其中进行矩阵、投影和矢量运算。
- 内置文件格式支持:你可以使用流行的 3D 建模软件导出文本格式的文件,然后使用 Three.js 加载,也可以使用 Three.js 自己的 JSON 格式或二进制格式。
- 扩展性很强:为 Three.js 添加新的特性或进行自定义优化是很容易的事情。如果你需要某个特殊的数据结构,那么只需要封装到 Three.js 即可。
- 支持 HTML5 Canvas:Three.js 不但支持 WebGL,而且还支持使用 Canvas2D、Css3D 和 SVG 进行渲染。在未兼容 WebGL 的环境中可以回退到其它的解决方案。
缺点
- 官网文档非常粗糙,对于新手极度不友好。
- 国内的相关资源匮乏。
- Three.js 所有的资料都是以英文格式存在,对国内的朋友来说又提高了门槛。
- Three.js 不是游戏引擎,一些游戏相关的功能没有封装在里面,如果需要相关的功能需要进行二次开发。
Three.js 与其他库的对比
库名 描述 相比 Three.js Babylon.js 是最好的 JavaScript 3D 游戏引擎,它能创建专业级三维游戏。主要以游戏开发和易用性为主 Three.js 比较全面,而 Babylon.js 专注于游戏方面。
Babylon.js 提供了对碰撞检测、场景重力、面向游戏的照相机,Three.js 本身不自带,需要依靠引入插件实现。
对于 WebGL 的封装,双方做得各有千秋,Three.js 浅一些,好处是易于扩展,易于向更底层学习;Babylon.js 深一些,好处是易用,但扩展难度大一些。
Three.js 的发展依靠社区推动,出来的比较早,发展比较成熟,Babylon.js 由微软公司在 2013 推出,文档和社区都比较健全,国内还不怎么火。
PlayCanvas 是一个基于 WebGL 游戏引擎的企业级开源 JavaScript 框架,它有许多的开发工具能帮你快速创建 3D 游戏 PlayCanvas 的优势在于它有云端的在线可视化编辑工具。 PlayCanvas 的扩展性不如 Three.js。 最主要是 PlayCanvas 不完全开源,还商业付费。 Cesium 是国外一个基于 JavaScript 编写的使用 WebGL 的地图引擎,支持 3D、2D、2.5D 形式的地图展示,可以自行绘制图形,高亮区域。 Cesium 是一个地图引擎,专注于 GIS,相关项目推荐使用它,其它项目还是算了。 至于库的扩展,其它的配套插件,以及周边的资源都不及 Three.js。
Three.js 在其库的扩展性,易用性以及功能方面有很好的优势。
Three.js-master 源码目录结构
- build:里面含有 Three.js 构建出来的 JavaScript 文件,可以直接引入使用,并有压缩版;
- docs:Three.js 的官方文档;
- editor:Three.js 的一个网页版的模型编辑器;
- examples:Three.js 的官方案例,如果全都学会,必将成为大神;
- src:这里面放置的全是编译 Three.js 的源文件;
- test:一些官方测试代码,我们一般用不到;
- utils:一些相关插件;
- 其他:开发环境搭建、开发所需要的文件,如果不对 Three.js 进行二次开发,用不到。
使用 Three.js
第一个案例
所有关于Threejs的代码均在https://github.com/Cenergy/webpack-threejs.git
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>第一个Three.js案例</title>
<style>
body {
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
</head>
<body onload="init()">
<script src="https://cdn.bootcss.com/three.js/108/three.js"></script>
<script>
//声明一些全局变量
var renderer, camera, scene, geometry, material, mesh;
//初始化渲染器
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高
document.body.appendChild(renderer.domElement); //添加到dom
}
//初始化场景
function initScene() {
scene = new THREE.Scene(); //实例化场景
}
//初始化相机
function initCamera() {
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
); //实例化相机
camera.position.set(0, 0, 15);
}
//创建模型
function initMesh() {
geometry = new THREE.BoxGeometry(2, 2, 2); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh(geometry, material); //创建网格
scene.add(mesh); //将网格添加到场景
}
//运行动画
function animate() {
requestAnimationFrame(animate); //循环调用函数
mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度
mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度
renderer.render(scene, camera); //渲染界面
}
//初始化函数,页面加载完成是调用
function init() {
initRenderer();
initScene();
initCamera();
initMesh();
animate();
}
</script>
</body>
</html>
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/4ce65006.html b/posts/4ce65006.html
index ff4b713c..2041ed58 100644
--- a/posts/4ce65006.html
+++ b/posts/4ce65006.html
@@ -1,3 +1,3 @@
-着色器之书09 - AIGISSS 着色器之书09
本文最后更新于:1 个月前
Patterns 图案
因为着色器按一个个像素执行,那么无论你重复一个图形多少次,计算的数量仍然是个常数。
本章中我们将综合我们目前所学的并应用在画布上。和前几章一样,我们的策略依然基于乘以空间坐标(0到1之间),这样我们的画的在0到1之间的图形就会重复地形成网格。
“网格提供一种基于人的直觉发明事物的框架,并且可以颠覆。自然的混沌肌理提供一种对比和秩序的迹象。从早期罗马浴场里的陶瓷图案到几何镶嵌,人们那时候就习惯用网格来点缀他们的生活。”10 PRINT, Mit Press, (2013)
首先让我们记住 fract()
函数。它返回一个数的分数部分,本质上是除1的余数(mod(x,1.0)
)。换句话说, fract()
返回小数点后的数。 我们单位化的坐标系变量 (st
) 已经是 0.0 到 1.0 之间的了。所以像下面这么做并没有必要:
1
2
3
4
5
6
7
void main(){
vec2 st = gl_FragCoord.xy/u_resolution;
vec3 color = vec3(0.0);
st = fract(st);
color = vec3(st,0.0);
gl_FragColor = vec4(color,1.0);
}
但如果我们放大单位化坐标系 — 比如说3倍 — 我们会得到三组 0 到 1 的线性插值的数列:第一组在 0-1 之间,第二组浮点数在 1-2 之间以及第三组在 2-3 之间的浮点数。
现在是时候在子空间(网格单元的空间)里画点什么了。取消27行的注释。(因为我们等比例放大了x和y坐标,所以不会改变坐标的比例,图形会和预期的一样。)
试试下面的练习来深入理解:
把空间乘以不同的数。试试用浮点数,还有分别给x和y不同的系数。
把这个平铺技巧做成一个可以反复使用的函数。
把画布分成 3 行 3 列。 指出如何定义行和列的线程的,并用这种方式改变显示着的图形。试着做一个井字棋。
在图案内部应用矩阵
鉴于每个细分或者说单元都是我们正在使用的单位化坐标系的小单元,我们可以对每个内部空间施以矩阵变换来平移,旋转和缩放。
想想怎么让这些图案有趣的动起来。考虑颜色,形状,运动的变换。做三种动画。
通过组合不同的形状重新创造更复杂的图案。
- 结合多层图案来制作你自己的 Scottish Tartan Patterns.
偏移图案
So let’s say we want to imitate a brick wall. Looking at the wall, you can see a half brick offset on x in every other row. How we can do that?
假如我们想要模仿砖墙。看,下面的墙,你是不是看到一半的砖在x方向上偏移了一半砖的长度,没隔一行偏移一次。我们如何实现?

第一步我们需要知道某行的线程是奇数还是偶数,以为我们可以通过奇偶来决定是否要在x方向上偏移那一行。
我们需要两段来解决这个问题
要判断我们的线程是一个奇数行或者偶数行,我们要用 2.0
的 mod()
。 然后根据结果是否大于 1.0
来判断。看一下下面的函数,取消最后两行的注释。
着色器之书09 - AIGISSS 着色器之书09
本文最后更新于:1 个月前
Patterns 图案
因为着色器按一个个像素执行,那么无论你重复一个图形多少次,计算的数量仍然是个常数。
本章中我们将综合我们目前所学的并应用在画布上。和前几章一样,我们的策略依然基于乘以空间坐标(0到1之间),这样我们的画的在0到1之间的图形就会重复地形成网格。
“网格提供一种基于人的直觉发明事物的框架,并且可以颠覆。自然的混沌肌理提供一种对比和秩序的迹象。从早期罗马浴场里的陶瓷图案到几何镶嵌,人们那时候就习惯用网格来点缀他们的生活。”10 PRINT, Mit Press, (2013)
首先让我们记住 fract()
函数。它返回一个数的分数部分,本质上是除1的余数(mod(x,1.0)
)。换句话说, fract()
返回小数点后的数。 我们单位化的坐标系变量 (st
) 已经是 0.0 到 1.0 之间的了。所以像下面这么做并没有必要:
1
2
3
4
5
6
7
void main(){
vec2 st = gl_FragCoord.xy/u_resolution;
vec3 color = vec3(0.0);
st = fract(st);
color = vec3(st,0.0);
gl_FragColor = vec4(color,1.0);
}
但如果我们放大单位化坐标系 — 比如说3倍 — 我们会得到三组 0 到 1 的线性插值的数列:第一组在 0-1 之间,第二组浮点数在 1-2 之间以及第三组在 2-3 之间的浮点数。
现在是时候在子空间(网格单元的空间)里画点什么了。取消27行的注释。(因为我们等比例放大了x和y坐标,所以不会改变坐标的比例,图形会和预期的一样。)
试试下面的练习来深入理解:
把空间乘以不同的数。试试用浮点数,还有分别给x和y不同的系数。
把这个平铺技巧做成一个可以反复使用的函数。
把画布分成 3 行 3 列。 指出如何定义行和列的线程的,并用这种方式改变显示着的图形。试着做一个井字棋。
在图案内部应用矩阵
鉴于每个细分或者说单元都是我们正在使用的单位化坐标系的小单元,我们可以对每个内部空间施以矩阵变换来平移,旋转和缩放。
想想怎么让这些图案有趣的动起来。考虑颜色,形状,运动的变换。做三种动画。
通过组合不同的形状重新创造更复杂的图案。
- 结合多层图案来制作你自己的 Scottish Tartan Patterns.
偏移图案
So let’s say we want to imitate a brick wall. Looking at the wall, you can see a half brick offset on x in every other row. How we can do that?
假如我们想要模仿砖墙。看,下面的墙,你是不是看到一半的砖在x方向上偏移了一半砖的长度,没隔一行偏移一次。我们如何实现?

第一步我们需要知道某行的线程是奇数还是偶数,以为我们可以通过奇偶来决定是否要在x方向上偏移那一行。
我们需要两段来解决这个问题
要判断我们的线程是一个奇数行或者偶数行,我们要用 2.0
的 mod()
。 然后根据结果是否大于 1.0
来判断。看一下下面的函数,取消最后两行的注释。
正如你所见,我们可以用一个 三元算符号 (第二行)来检查 2.0
的mod()
(余数)小于 1.0
或者类似地,我们用 step()
函数做相同的操作,但(其实)更快。为什么呢? 因为虽然要知道每个显卡如何优化和编译代码并不容易,但是可以安全地假设内置函数总比非内置的函数快。任何时候你都以调用内置函数,干嘛不用呢!
现在我们有这些找出奇数的方程,这样我们就可以给奇数行一个偏移量,然后就可以把 砖块 做出拼砖的效果。下面代码的第14行便是我们用来“侦测”奇数行,并予之半个单位在x上的偏移的。注意到对偶数行,函数的返回值是 0.0
, 0.0
乘以 0.5
得到一个 0.0
的偏移。 但是奇数行我们用函数的返回值, 1.0
, 乘以偏移量 0.5
,这样便向坐标系的 x
轴偏移了 0.5
。
现在试着取消32行的注释 — 拉伸长宽比来模仿“现代砖块”的长宽比。通过取消第40行的的代码,你可以注意到坐标系统是如何看起来映射到红绿色的。
试着根据时间变化对偏移量做动画。
另做一个动画,让偶数行向左移,奇数行向右移动。
能不能根据列重复这样的效果?
试着结合 x
和 y
轴的偏移来得到下面这样的效果:
Truchet 瓷砖
目前我们学了如何区分奇数行/列或偶数行/列,(类似的),(我们也)可能再用(这个技巧)根据位置来设计元素。 考虑到 Truchet Tiles 的例子,即一个单一设计元素可以以四种不同的方式呈现:

通过改变对角瓷砖的图案,便可能组成无限种复杂设计的可能。

仔细观察 rotateTilePattern()
函数, 它把坐标空间细分成四个单元并赋予每一个旋转值。
注释,取消注释,以及复制第69到72行来创作新的设计。
把黑白三角变成其他元素,例如:半圆,旋转的方形或直线。
编写根据元素自身位置旋转的图形代码。
创作一个根据其位置改变其他属性的图案。
想想其他能用这章原理的案例,不一定是图案. (例: 易经卦)
制定自己的规则
制作程序图案是种寻找最小可重复元素的古老练习(灵修)。我们作为长时间使用网格和图案来装饰织物、地面和物品的镶边物种:从古希腊的弯曲图案,到中国的窗栅设计,重复和变化的愉悦吸引我们的想象。花些时间浏览 decorative patterns 并看看在漫长的历史里,艺术家和设计师是如何寻找在秩序的预测性和(由)混沌和衍变(产生)的惊奇之间的边界的。从阿拉伯几何图案,到斑斓的非洲编制艺术,这里有一整个图案的宇宙要学习。

这章我们就结束算法绘图部分了。在接下来的章节,我们会学习如何把熵(其实就是混乱和随机的意思)加入到我们的着色器中来产生生成设计。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/4ef7fe51.html b/posts/4ef7fe51.html
index 6554cecd..54a71576 100644
--- a/posts/4ef7fe51.html
+++ b/posts/4ef7fe51.html
@@ -1 +1 @@
-mapbox的矢量切片工具:tippecanoe - AIGISSS mapbox的矢量切片工具:tippecanoe
本文最后更新于:1 个月前
矢量切片工具:tippecanoe
Tippecanoe 用于将 GeoJSON, Geobuf, 或者 CSV 格式的矢量要素转换为矢量瓦片。
目的
Tippecanoe 的目的是将数据制作为比例独立的视图,以使在任何缩放级别下,你都可以看到数据的密度和细节,而不是将数据简化或聚合。
如果你提供的是 OpenStreetMap 所有的数据,在小比例尺下,你应该看到类似于All Streets的地图,而不是州际道路地图。
如果你提供的是洛杉矶的所有详细的建筑数据,并且将地图缩放到小比例尺下,绝大部分的单体建筑将不再可辨,但是你应该可以看到每个街区的范围和变化。
如果你提供的是一年内 twitter 推文的定位数据集,你应该可以发现所有兴趣点之间的关联和热门的旅游路线。
安装
OSX 操作系统使用 Homebrew 安装:
1
$ brew install tippecanoe
Ubuntu 系统最简单的方式是从源码中构建:
1
2
3
4
$ git clone git@github.com:mapbox/tippecanoe.git
$ cd tippecanoe
$ make -j
$ make install
Window系统的最简单的方式是安装一个Ubuntu系统:
在windows10 第3个稳定版发布以后,支持内嵌的linux系统,下面我们一起来看看,怎么使用它内部自带的linux系统。
https://jingyan.baidu.com/article/c85b7a64a56c7f003aac954f.html
如果编译中出现问题,可能是你的C++编译器需要升级,或者缺少必要的依赖包,详细请查看文档。
使用
1
$ tippecanoe -o file.mbtiles [options] [file.json file.json.gz file.geobuf ...]
如果没有指定文件,会从默认路径读取 GeoJSON 文件;如果指定了多个文件,每一个文件将会被当做一个图层。
GeoJSON 要素不一定非得包含在 FeatureCollection 中。你可以将多个 GeoJSON 要素或者文件合并。
Try this first
如果你不确定使用什么选项,请尝试一下命令:
1
$ tippecanoe -o out.mbtiles -zg --drop-densest-as-needed in.geojson
使用-zg
选项,Tippecanoe 将自动选择一个可以反映原始数据精度的最大级别(如果结果没有达到你想要的效果,你也可以使用-z
选项手动设置最大级别)。
如果生产出的切片太大,可以使用 --drop-densest-as-needed
选项,来让Tippecanoe自动删除各个级别下最不可见的要素。(如果它删除了太多的要素,你可以使用-x
选项来删除不必要的属性字段)
选项
tippecanoe 切片有很多选项,但是大部分情况下你并不想要使用它们,除了使用 -o output.mbtiles
来定义输出瓦片文件名,或者再加上-f
选项来强制删除同名文件。
如果你不确定需要切片的最大级别,-zg
选项可以根据源数据自动计算出一个最大级别。
通常,在最大切片级别以下的级别,tippecanoe 会舍弃部分点要素,以防止瓦片过大。如果你的数据集本来就不大,你想要保留所有要素,可以使用-r1
选项。如果你确实是想要简化数据,但是又不想简化得过于稀疏,可以使用 -B
选项设置一个小于最大级别的数值。
通过以上设置,如果你的切片仍然很大,你可以使用 --drop-densest-as-needed
选项来进一步简化要素。
如果你的要素包含很多属性,你可以使用-y
选项来选择只保留你给定的字段。
如果你的GeoJSON 文件是格式化后的,使用-p
可以加快文件读取。
输出选项
-o file.mbtiles
或 --output=file.mbtiles
:指定输出文件名-e directory
或 --output-to-directory=directory
:指定输出文件路径-f
或 --force
:若存在同名文件则删除-F
或 --allow-existing
瓦片集属性选项
-n name
或 --name=name
: 给瓦片集设置一个易读的名字-A text
或 --attribution=text
: 瓦片集-N description
或 --description=description
: 瓦片集描述
输入文件和图层名
name.json
或 name.geojson
:读取 GeoJSON 文件
name.json.gz
或 name.geojson.gz
:读取 GeoJSON 压缩文件
name.geobuf
:读取 Geobuf 文件
name.csv
:读取 CSV 文件
-l name
或 --layer=name
: 使用自定义图层名,而不是默认的输入文件名作为图层名,如果有多个输入文件,将合并为一个图层,除非使用-L
选项来分别指定图层名。
-L name:file.json
或 --named-layer=name:file.json
:定义每个文件的对应的图层名
-L{layer-json}
或 --named-layer={layer-json}
: 通过 json 对象定义图层。示例:
1
tippecanoe -z5 -o world.mbtiles -L'{"file":"ne_10m_admin_0_countries.json", "layer":"countries", "description":"Natural Earth countries"}'
坐标系
-s projection
或 --projection=projection
: 给定输入文件的坐标系统。当前支持的坐标系有EPSG:4326
(WGS84,默认值)、EPSG:3857
(Web Mercator)。请尽量使用 WGS84 坐标系统的数据集。
切片级别
-z zoom
或 --maximum-zoom=zoom
:切片的最大级别(默认为14)-zg
或 --maximum-zoom=g
: 根据数据的密集程度自动计算一个最大级别-Z zoom
或 --minimum-zoom=zoom
: 切片的最小级别(默认0)-ae
或 --extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除-R zoom/x/y
或 --one-tile=zoom/x/y
:
如果你知道你想要的切片的数据精度,那么你就可以根据以下表格来设置切片级别:
级别 精度 (英尺) 精度 (m) -z0
32000 ft 10000 m -z1
16000 ft 5000 m -z2
8000 ft 2500 m -z3
4000 ft 1250 m -z4
2000 ft 600 m -z5
1000 ft 300 m -z6
500 ft 150 m -z7
250 ft 80 m -z8
125 ft 40 m -z9
64 ft 20 m -z10
32 ft 10 m -z11
16 ft 5 m -z12
8 ft 2 m -z13
4 ft 1 m -z14
2 ft 0.5 m -z15
1 ft 0.25 m
属性筛选
-x name
或 --exclude=name
: 指定切片中应剔除的字段。-y name
或 --include=name
: 指定切片中应包含的字段。
Cookbook
线要素(全球铁路),在所有级别可见
1
2
3
4
5
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_railroads.zip
unzip ne_10m_railroads.zip
ogr2ogr -f GeoJSON ne_10m_railroads.geojson ne_10m_railroads.shp
tippecanoe -zg -o ne_10m_railroads.mbtiles --drop-densest-as-needed --extend-zooms-if-still-dropping ne_10m_railroads.geojson
-zg
: 自动选择最大级别;--drop-densest-as-needed
: 如果在小级别下瓦片太大,该选项将自动简化要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
不连续的面要素(美国罗德岛),在所有级别可见
1
2
3
4
curl -L -O https://usbuildingdata.blob.core.windows.net/usbuildings-v1-1/RhodeIsland.zip
unzip RhodeIsland.zip
tippecanoe -zg -o RhodeIsland.mbtiles --drop-densest-as-needed --extend-zooms-if-still-dropping RhodeIsland.geojson
-zg
: 自动选择最大级别;--drop-densest-as-needed
: 如果在小级别下瓦片太大,该选项将自动简化要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
连续的面要素(行政区划),在所有级别可见
1
2
3
4
5
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_1_states_provinces.zip
unzip -o ne_10m_admin_1_states_provinces.zip
ogr2ogr -f GeoJSON ne_10m_admin_1_states_provinces.geojson ne_10m_admin_1_states_provinces.shp
tippecanoe -zg -o ne_10m_admin_1_states_provinces.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping ne_10m_admin_1_states_provinces.geojson
-zg
: 自动选择最大级别;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;
海量点数据(公交车GPS轨迹数据),可视化,在所有级别可见
1
2
3
curl -L -O ftp://avl-data.sfmta.com/avl_data/avl_raw/sfmtaAVLRawData01012013.csv
sed 's/PREDICTABLE.*/PREDICTABLE/' sfmtaAVLRawData01012013.csv > sfmta.csv
tippecanoe -zg -o sfmta.mbtiles --drop-densest-as-needed --extend-zooms-if-still-dropping sfmta.csv
(sed
命令用于清除不必要的字段)
-zg
: 自动选择最大级别;--drop-densest-as-needed
: 如果在小级别下瓦片太大,该选项将自动简化要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
低级别显示国家边界,高级别显示州边界
1
2
3
4
5
6
7
8
9
10
11
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip
unzip ne_10m_admin_0_countries.zip
ogr2ogr -f GeoJSON ne_10m_admin_0_countries.geojson ne_10m_admin_0_countries.shp
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_1_states_provinces.zip
unzip -o ne_10m_admin_1_states_provinces.zip
ogr2ogr -f GeoJSON ne_10m_admin_1_states_provinces.geojson ne_10m_admin_1_states_provinces.shp
tippecanoe -z3 -o countries-z3.mbtiles --coalesce-densest-as-needed ne_10m_admin_0_countries.geojson
tippecanoe -zg -Z4 -o states-Z4.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping ne_10m_admin_1_states_provinces.geojson
tile-join -o states-countries.mbtiles countries-z3.mbtiles states-Z4.mbtiles
- Countries:
-z3
: 最大切片级别为3,即只切 0 - 3 级别的瓦片;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;
- States and Provinces:
-Z4
: 最小切片级别为4;-zg
: 自动选择最大级别;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
多个数据源切片为独立的图层
1
2
3
4
5
6
7
8
9
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_17_county10.zip
unzip tl_2010_17_county10.zip
ogr2ogr -f GeoJSON tl_2010_17_county10.geojson tl_2010_17_county10.shp
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_18_county10.zip
unzip tl_2010_18_county10.zip
ogr2ogr -f GeoJSON tl_2010_18_county10.geojson tl_2010_18_county10.shp
tippecanoe -zg -o counties-separate.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping tl_2010_17_county10.geojson tl_2010_18_county10.geojson
-zg
: 自动选择最大级别;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
多个数据源切片并合并为一个图层
1
2
3
4
5
6
7
8
9
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_17_county10.zip
unzip tl_2010_17_county10.zip
ogr2ogr -f GeoJSON tl_2010_17_county10.geojson tl_2010_17_county10.shp
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_18_county10.zip
unzip tl_2010_18_county10.zip
ogr2ogr -f GeoJSON tl_2010_18_county10.geojson tl_2010_18_county10.shp
tippecanoe -zg -o counties-merged.mbtiles -l counties --coalesce-densest-as-needed --extend-zooms-if-still-dropping tl_2010_17_county10.geojson tl_2010_18_county10.geojson
-l counties
: 图层名默认为文件名,也可以使用该选项自定义;
tile-join
用于合并或复制矢量瓦片。
tippecanoe-decode
用于将矢量瓦片逆向转换为 GeoJSON。
tippecanoe-enumerate
用于列举mbtiles中的矢量瓦片。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+mapbox的矢量切片工具:tippecanoe - AIGISSS mapbox的矢量切片工具:tippecanoe
本文最后更新于:1 个月前
矢量切片工具:tippecanoe
Tippecanoe 用于将 GeoJSON, Geobuf, 或者 CSV 格式的矢量要素转换为矢量瓦片。
目的
Tippecanoe 的目的是将数据制作为比例独立的视图,以使在任何缩放级别下,你都可以看到数据的密度和细节,而不是将数据简化或聚合。
如果你提供的是 OpenStreetMap 所有的数据,在小比例尺下,你应该看到类似于All Streets的地图,而不是州际道路地图。
如果你提供的是洛杉矶的所有详细的建筑数据,并且将地图缩放到小比例尺下,绝大部分的单体建筑将不再可辨,但是你应该可以看到每个街区的范围和变化。
如果你提供的是一年内 twitter 推文的定位数据集,你应该可以发现所有兴趣点之间的关联和热门的旅游路线。
安装
OSX 操作系统使用 Homebrew 安装:
1
$ brew install tippecanoe
Ubuntu 系统最简单的方式是从源码中构建:
1
2
3
4
$ git clone git@github.com:mapbox/tippecanoe.git
$ cd tippecanoe
$ make -j
$ make install
Window系统的最简单的方式是安装一个Ubuntu系统:
在windows10 第3个稳定版发布以后,支持内嵌的linux系统,下面我们一起来看看,怎么使用它内部自带的linux系统。
https://jingyan.baidu.com/article/c85b7a64a56c7f003aac954f.html
如果编译中出现问题,可能是你的C++编译器需要升级,或者缺少必要的依赖包,详细请查看文档。
使用
1
$ tippecanoe -o file.mbtiles [options] [file.json file.json.gz file.geobuf ...]
如果没有指定文件,会从默认路径读取 GeoJSON 文件;如果指定了多个文件,每一个文件将会被当做一个图层。
GeoJSON 要素不一定非得包含在 FeatureCollection 中。你可以将多个 GeoJSON 要素或者文件合并。
Try this first
如果你不确定使用什么选项,请尝试一下命令:
1
$ tippecanoe -o out.mbtiles -zg --drop-densest-as-needed in.geojson
使用-zg
选项,Tippecanoe 将自动选择一个可以反映原始数据精度的最大级别(如果结果没有达到你想要的效果,你也可以使用-z
选项手动设置最大级别)。
如果生产出的切片太大,可以使用 --drop-densest-as-needed
选项,来让Tippecanoe自动删除各个级别下最不可见的要素。(如果它删除了太多的要素,你可以使用-x
选项来删除不必要的属性字段)
选项
tippecanoe 切片有很多选项,但是大部分情况下你并不想要使用它们,除了使用 -o output.mbtiles
来定义输出瓦片文件名,或者再加上-f
选项来强制删除同名文件。
如果你不确定需要切片的最大级别,-zg
选项可以根据源数据自动计算出一个最大级别。
通常,在最大切片级别以下的级别,tippecanoe 会舍弃部分点要素,以防止瓦片过大。如果你的数据集本来就不大,你想要保留所有要素,可以使用-r1
选项。如果你确实是想要简化数据,但是又不想简化得过于稀疏,可以使用 -B
选项设置一个小于最大级别的数值。
通过以上设置,如果你的切片仍然很大,你可以使用 --drop-densest-as-needed
选项来进一步简化要素。
如果你的要素包含很多属性,你可以使用-y
选项来选择只保留你给定的字段。
如果你的GeoJSON 文件是格式化后的,使用-p
可以加快文件读取。
输出选项
-o file.mbtiles
或 --output=file.mbtiles
:指定输出文件名-e directory
或 --output-to-directory=directory
:指定输出文件路径-f
或 --force
:若存在同名文件则删除-F
或 --allow-existing
瓦片集属性选项
-n name
或 --name=name
: 给瓦片集设置一个易读的名字-A text
或 --attribution=text
: 瓦片集-N description
或 --description=description
: 瓦片集描述
输入文件和图层名
name.json
或 name.geojson
:读取 GeoJSON 文件
name.json.gz
或 name.geojson.gz
:读取 GeoJSON 压缩文件
name.geobuf
:读取 Geobuf 文件
name.csv
:读取 CSV 文件
-l name
或 --layer=name
: 使用自定义图层名,而不是默认的输入文件名作为图层名,如果有多个输入文件,将合并为一个图层,除非使用-L
选项来分别指定图层名。
-L name:file.json
或 --named-layer=name:file.json
:定义每个文件的对应的图层名
-L{layer-json}
或 --named-layer={layer-json}
: 通过 json 对象定义图层。示例:
1
tippecanoe -z5 -o world.mbtiles -L'{"file":"ne_10m_admin_0_countries.json", "layer":"countries", "description":"Natural Earth countries"}'
坐标系
-s projection
或 --projection=projection
: 给定输入文件的坐标系统。当前支持的坐标系有EPSG:4326
(WGS84,默认值)、EPSG:3857
(Web Mercator)。请尽量使用 WGS84 坐标系统的数据集。
切片级别
-z zoom
或 --maximum-zoom=zoom
:切片的最大级别(默认为14)-zg
或 --maximum-zoom=g
: 根据数据的密集程度自动计算一个最大级别-Z zoom
或 --minimum-zoom=zoom
: 切片的最小级别(默认0)-ae
或 --extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除-R zoom/x/y
或 --one-tile=zoom/x/y
:
如果你知道你想要的切片的数据精度,那么你就可以根据以下表格来设置切片级别:
级别 精度 (英尺) 精度 (m) -z0
32000 ft 10000 m -z1
16000 ft 5000 m -z2
8000 ft 2500 m -z3
4000 ft 1250 m -z4
2000 ft 600 m -z5
1000 ft 300 m -z6
500 ft 150 m -z7
250 ft 80 m -z8
125 ft 40 m -z9
64 ft 20 m -z10
32 ft 10 m -z11
16 ft 5 m -z12
8 ft 2 m -z13
4 ft 1 m -z14
2 ft 0.5 m -z15
1 ft 0.25 m
属性筛选
-x name
或 --exclude=name
: 指定切片中应剔除的字段。-y name
或 --include=name
: 指定切片中应包含的字段。
Cookbook
线要素(全球铁路),在所有级别可见
1
2
3
4
5
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_railroads.zip
unzip ne_10m_railroads.zip
ogr2ogr -f GeoJSON ne_10m_railroads.geojson ne_10m_railroads.shp
tippecanoe -zg -o ne_10m_railroads.mbtiles --drop-densest-as-needed --extend-zooms-if-still-dropping ne_10m_railroads.geojson
-zg
: 自动选择最大级别;--drop-densest-as-needed
: 如果在小级别下瓦片太大,该选项将自动简化要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
不连续的面要素(美国罗德岛),在所有级别可见
1
2
3
4
curl -L -O https://usbuildingdata.blob.core.windows.net/usbuildings-v1-1/RhodeIsland.zip
unzip RhodeIsland.zip
tippecanoe -zg -o RhodeIsland.mbtiles --drop-densest-as-needed --extend-zooms-if-still-dropping RhodeIsland.geojson
-zg
: 自动选择最大级别;--drop-densest-as-needed
: 如果在小级别下瓦片太大,该选项将自动简化要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
连续的面要素(行政区划),在所有级别可见
1
2
3
4
5
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_1_states_provinces.zip
unzip -o ne_10m_admin_1_states_provinces.zip
ogr2ogr -f GeoJSON ne_10m_admin_1_states_provinces.geojson ne_10m_admin_1_states_provinces.shp
tippecanoe -zg -o ne_10m_admin_1_states_provinces.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping ne_10m_admin_1_states_provinces.geojson
-zg
: 自动选择最大级别;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;
海量点数据(公交车GPS轨迹数据),可视化,在所有级别可见
1
2
3
curl -L -O ftp://avl-data.sfmta.com/avl_data/avl_raw/sfmtaAVLRawData01012013.csv
sed 's/PREDICTABLE.*/PREDICTABLE/' sfmtaAVLRawData01012013.csv > sfmta.csv
tippecanoe -zg -o sfmta.mbtiles --drop-densest-as-needed --extend-zooms-if-still-dropping sfmta.csv
(sed
命令用于清除不必要的字段)
-zg
: 自动选择最大级别;--drop-densest-as-needed
: 如果在小级别下瓦片太大,该选项将自动简化要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
低级别显示国家边界,高级别显示州边界
1
2
3
4
5
6
7
8
9
10
11
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip
unzip ne_10m_admin_0_countries.zip
ogr2ogr -f GeoJSON ne_10m_admin_0_countries.geojson ne_10m_admin_0_countries.shp
curl -L -O https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_1_states_provinces.zip
unzip -o ne_10m_admin_1_states_provinces.zip
ogr2ogr -f GeoJSON ne_10m_admin_1_states_provinces.geojson ne_10m_admin_1_states_provinces.shp
tippecanoe -z3 -o countries-z3.mbtiles --coalesce-densest-as-needed ne_10m_admin_0_countries.geojson
tippecanoe -zg -Z4 -o states-Z4.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping ne_10m_admin_1_states_provinces.geojson
tile-join -o states-countries.mbtiles countries-z3.mbtiles states-Z4.mbtiles
- Countries:
-z3
: 最大切片级别为3,即只切 0 - 3 级别的瓦片;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;
- States and Provinces:
-Z4
: 最小切片级别为4;-zg
: 自动选择最大级别;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
多个数据源切片为独立的图层
1
2
3
4
5
6
7
8
9
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_17_county10.zip
unzip tl_2010_17_county10.zip
ogr2ogr -f GeoJSON tl_2010_17_county10.geojson tl_2010_17_county10.shp
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_18_county10.zip
unzip tl_2010_18_county10.zip
ogr2ogr -f GeoJSON tl_2010_18_county10.geojson tl_2010_18_county10.shp
tippecanoe -zg -o counties-separate.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping tl_2010_17_county10.geojson tl_2010_18_county10.geojson
-zg
: 自动选择最大级别;--coalesce-densest-as-needed
: 如果瓦片在低级别或中等级别下比较大,该选项将合并要素;--extend-zooms-if-still-dropping
: 如果在大级别下瓦片仍然很大,它将自动增加最大级别,以使最大级别下没有要素被删除;
多个数据源切片并合并为一个图层
1
2
3
4
5
6
7
8
9
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_17_county10.zip
unzip tl_2010_17_county10.zip
ogr2ogr -f GeoJSON tl_2010_17_county10.geojson tl_2010_17_county10.shp
curl -L -O https://www2.census.gov/geo/tiger/TIGER2010/COUNTY/2010/tl_2010_18_county10.zip
unzip tl_2010_18_county10.zip
ogr2ogr -f GeoJSON tl_2010_18_county10.geojson tl_2010_18_county10.shp
tippecanoe -zg -o counties-merged.mbtiles -l counties --coalesce-densest-as-needed --extend-zooms-if-still-dropping tl_2010_17_county10.geojson tl_2010_18_county10.geojson
-l counties
: 图层名默认为文件名,也可以使用该选项自定义;
tile-join
用于合并或复制矢量瓦片。
tippecanoe-decode
用于将矢量瓦片逆向转换为 GeoJSON。
tippecanoe-enumerate
用于列举mbtiles中的矢量瓦片。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/5796179d.html b/posts/5796179d.html
index 28a07706..81e2ad2b 100644
--- a/posts/5796179d.html
+++ b/posts/5796179d.html
@@ -1 +1 @@
-mapboxgl中解决图片动画的性能问题 - AIGISSS mapboxgl中解决图片动画的性能问题
本文最后更新于:8 个月前
mapbox-gl
中的动画图片是不断请求服务器得来的,详情请看地址,虽然是实现了,但一直请求不是好的解决办法,特别还是mapbox-gl
这种追求性能极致的来说。我们可以有多种解决方法。
方案一:使用updateImage来解决
首先还是加载图层资源。
1
2
3
4
5
map.addSource('layerSourceName', {
type: 'image',
url: imageUrl,
coordinates: coor,
});
然后加载这个资源到图层。
最重要的来了,定时显示图片的环节。
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
//构造函数如下:
constructor(id, option) {
const { distance, level, coordinate, frameCount, urlPrifix } = option;
this._id = id;
this._distance = distance;
this._level = level;
this._coordinate = coordinate;
this._frameCount = frameCount;
this._currentImage = this._getRandomInt(1, option.frameCount - 1);
this._layerSourceName = id;
this._layerName = id;
this._urlPrifix = urlPrifix;
this._map = null;
this._imagesMap = new Map();
}
// 定时显示图片
tick() {
let index = 1;
const radarEffect = () => {
if (index % 4 !== 0) {
index += 1;
} else {
index = 1;
const source = this.map.getSource(this._layerSourceName);
this._currentImage = (this._currentImage + 1) % this._frameCount;
if (!this._imagesMap.has(this._currentImage)) {
source.updateImage({ url: this._getPath(this._prefix, this._currentImage) });
if (source.image) {
this._imagesMap.set(this._currentImage, source.image);
}
} else {
source.texture = null;
source.image = this._imagesMap.get(this._currentImage);
source._finishLoading.call(source);
}
}
window.requestAnimationFrame(radarEffect);
};
window.requestAnimationFrame(radarEffect);
}
_getPath() {
return `${this._urlPrifix}/${this._currentImage}.png`;
}
方案二:使用精灵图来解决。
话不多说,talk is cheap ,show my code
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
const pulsingDot = {
width: size,
height: size,
data: new Uint8Array(size * size * 4),
// get rendering context for the map canvas when layer is added to the map
onAdd() {
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext('2d');
const image = new Image();
image.src = './green.png';
this.image = image;
// 创建一个精灵图对象
const sprite = new Sprite({
canvas,
image,
numberOfFrames: 10,
ticksPerFrame: 0,
row: 7,
column: 7,
x: 0,
y: 0,
});
this.sprite = sprite;
},
// called once before every frame where the icon will be used
render() {
const context = this.context;
// console.log(`rdapp: render -> this`, this);
// draw outer circle
context.clearRect(0, 0, this.width, this.height);
this.sprite.update();
this.sprite.render();
this.data = context.getImageData(0, 0, this.width, this.height).data;
// continuously repaint the map, resulting in the smooth animation of the dot
map.triggerRepaint();
// return `true` to let the map know that the image was updated
return true;
},
};
map.on('load', () => {
map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 });
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: mapConfig.center,
},
},
],
},
});
map.addLayer({
id: 'points',
type: 'symbol',
source: 'points',
layout: {
'icon-image': 'pulsing-dot',
'icon-rotation-alignment': 'map',
},
});
});
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+mapboxgl中解决图片动画的性能问题 - AIGISSS mapboxgl中解决图片动画的性能问题
本文最后更新于:1 分钟前
mapbox-gl
中的动画图片是不断请求服务器得来的,详情请看地址,虽然是实现了,但一直请求不是好的解决办法,特别还是mapbox-gl
这种追求性能极致的来说。我们可以有多种解决方法。
方案一:使用updateImage来解决
首先还是加载图层资源。
1
2
3
4
5
map.addSource('layerSourceName', {
type: 'image',
url: imageUrl,
coordinates: coor,
});
然后加载这个资源到图层。
最重要的来了,定时显示图片的环节。
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
//构造函数如下:
constructor(id, option) {
const { distance, level, coordinate, frameCount, urlPrifix } = option;
this._id = id;
this._distance = distance;
this._level = level;
this._coordinate = coordinate;
this._frameCount = frameCount;
this._currentImage = this._getRandomInt(1, option.frameCount - 1);
this._layerSourceName = id;
this._layerName = id;
this._urlPrifix = urlPrifix;
this._map = null;
this._imagesMap = new Map();
}
// 定时显示图片
tick() {
let index = 1;
const radarEffect = () => {
if (index % 4 !== 0) {
index += 1;
} else {
index = 1;
const source = this.map.getSource(this._layerSourceName);
this._currentImage = (this._currentImage + 1) % this._frameCount;
if (!this._imagesMap.has(this._currentImage)) {
source.updateImage({ url: this._getPath(this._prefix, this._currentImage) });
if (source.image) {
this._imagesMap.set(this._currentImage, source.image);
}
} else {
source.texture = null;
source.image = this._imagesMap.get(this._currentImage);
source._finishLoading.call(source);
}
}
window.requestAnimationFrame(radarEffect);
};
window.requestAnimationFrame(radarEffect);
}
_getPath() {
return `${this._urlPrifix}/${this._currentImage}.png`;
}
方案二:使用精灵图来解决。
话不多说,talk is cheap ,show my code
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
const pulsingDot = {
width: size,
height: size,
data: new Uint8Array(size * size * 4),
// get rendering context for the map canvas when layer is added to the map
onAdd() {
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext('2d');
const image = new Image();
image.src = './green.png';
this.image = image;
// 创建一个精灵图对象
const sprite = new Sprite({
canvas,
image,
numberOfFrames: 10,
ticksPerFrame: 0,
row: 7,
column: 7,
x: 0,
y: 0,
});
this.sprite = sprite;
},
// called once before every frame where the icon will be used
render() {
const context = this.context;
// console.log(`rdapp: render -> this`, this);
// draw outer circle
context.clearRect(0, 0, this.width, this.height);
this.sprite.update();
this.sprite.render();
this.data = context.getImageData(0, 0, this.width, this.height).data;
// continuously repaint the map, resulting in the smooth animation of the dot
map.triggerRepaint();
// return `true` to let the map know that the image was updated
return true;
},
};
map.on('load', () => {
map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 });
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: mapConfig.center,
},
},
],
},
});
map.addLayer({
id: 'points',
type: 'symbol',
source: 'points',
layout: {
'icon-image': 'pulsing-dot',
'icon-rotation-alignment': 'map',
},
});
});
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/5a12ed89.html b/posts/5a12ed89.html
index 0e7472f5..62dbe4dd 100644
--- a/posts/5a12ed89.html
+++ b/posts/5a12ed89.html
@@ -1 +1 @@
-JavaScript与有限状态机 - AIGISSS JavaScript与有限状态机
本文最后更新于:1 个月前
有限状态机
英语全称:finite-state machine,缩写:FSM。又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。
有限状态机是一个非常有用的模型,可以模拟世界上大部分事物。

简单说,它有三个特征:
- 状态总数(state)是有限的。
- 任一时刻,只处在一种状态之中。
- 某种条件下,会从一种状态转变(transition)到另一种状态。
它对JavaScript的意义在于,很多对象可以写成有限状态机。
举例来说,网页上有一个菜单元素。鼠标悬停的时候,菜单显示;鼠标移开的时候,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。
代码可以写成下面这样:
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
var menu = {
// 当前状态
currentState: 'hide',
// 绑定事件
initialize: function() {
var self = this;
self.on("hover", self.transition);
},
// 状态转换
transition: function(event){
switch(this.currentState) {
case "hide":
this.currentState = 'show';
doSomething();
break;
case "show":
this.currentState = 'hide';
doSomething();
break;
default:
console.log('Invalid State!');
break;
}
}
};
可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。
另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题。有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。
下面介绍一个有限状态机的函数库Javascript Finite State Machine。这个库非常好懂,可以帮助我们加深理解,而且功能一点都不弱。
该库提供一个全局对象StateMachine,使用该对象的create方法,可以生成有限状态机的实例。
1
var fsm = StateMachine.create();
生成的时候,需要提供一个参数对象,用来描述实例的性质。比如,交通信号灯(红绿灯)可以这样描述:
1
2
3
4
5
6
7
8
9
10
11
12
var fsm = StateMachine.create({
initial: 'green',
events: [
{ name: 'warn', from: 'green', to: 'yellow' },
{ name: 'stop', from: 'yellow', to: 'red' },
{ name: 'ready', from: 'red', to: 'yellow' },
{ name: 'go', from: 'yellow', to: 'green' }
]
});
交通信号灯的初始状态(initial)为green,events属性是触发状态改变的各种事件,比如warn事件使得green状态变成yellow状态,stop事件使得yellow状态变成red状态等等。
生成实例以后,就可以随时查询当前状态。
- fsm.current :返回当前状态。
- fsm.is(s) :返回一个布尔值,表示状态s是否为当前状态。
- fsm.can(e) :返回一个布尔值,表示事件e是否能在当前状态触发。
- fsm.cannot(e) :返回一个布尔值,表示事件e是否不能在当前状态触发。
Javascript Finite State Machine允许为每个事件指定两个回调函数,以warn事件为例:
- onbeforewarn:在warn事件发生之前触发。
- onafterwarn(可简写成onwarn) :在warn事件发生之后触发。
同时,它也允许为每个状态指定两个回调函数,以green状态为例:
- onleavegreen :在离开green状态时触发。
- onentergreen(可简写成ongreen) :在进入green状态时触发。
假定warn事件使得状态从green变为yellow,上面四类回调函数的发生顺序如下:onbeforewarn → onleavegreen → onenteryellow → onafterwarn。
除了为每个事件和状态单独指定回调函数,还可以为所有的事件和状态指定通用的回调函数。
- onbeforeevent :任一事件发生之前触发。
- onleavestate :离开任一状态时触发。
- onenterstate :进入任一状态时触发。
- onafterevent :任一事件结束后触发。
如果事件的回调函数里面有异步操作(比如与服务器进行Ajax通信),这时我们可能希望等到异步操作结束,再发生状态改变。这就要用到transition方法。
1
2
3
4
5
6
7
fsm.onleavegreen = function(){
light.fadeOut('slow', function() {
fsm.transition();
});
return StateMachine.ASYNC;
};
上面代码的回调函数里面,有一个异步操作(light.fadeOut)。如果不希望状态立即改变,就要让回调函数返回StateMachine.ASYNC,表示状态暂时不改变;等到异步操作结束,再调用transition方法,使得状态发生改变。
Javascript Finite State Machine还允许指定错误处理函数,当发生了当前状态不可能发生的事件时自动触发。
1
2
3
4
5
6
7
var fsm = StateMachine.create({
// ...
error: function(eventName, from, to, args, errorCode, errorMessage) {
return 'event ' + eventName + ': ' + errorMessage;
},
// ...
});
比如,当前状态是green,理论上这时只可能发生warn事件。要是这时发生了stop事件,就会触发上面的错误处理函数。
Javascript Finite State Machine的基本用法就是上面这些,更详细的介绍可以参见它的主页。
原文来自:http://www.ruanyifeng.com/blog/2013/09/finite-state_machine_for_javascript.html
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+JavaScript与有限状态机 - AIGISSS JavaScript与有限状态机
本文最后更新于:1 分钟前
有限状态机
英语全称:finite-state machine,缩写:FSM。又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。
有限状态机是一个非常有用的模型,可以模拟世界上大部分事物。

简单说,它有三个特征:
- 状态总数(state)是有限的。
- 任一时刻,只处在一种状态之中。
- 某种条件下,会从一种状态转变(transition)到另一种状态。
它对JavaScript的意义在于,很多对象可以写成有限状态机。
举例来说,网页上有一个菜单元素。鼠标悬停的时候,菜单显示;鼠标移开的时候,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。
代码可以写成下面这样:
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
var menu = {
// 当前状态
currentState: 'hide',
// 绑定事件
initialize: function() {
var self = this;
self.on("hover", self.transition);
},
// 状态转换
transition: function(event){
switch(this.currentState) {
case "hide":
this.currentState = 'show';
doSomething();
break;
case "show":
this.currentState = 'hide';
doSomething();
break;
default:
console.log('Invalid State!');
break;
}
}
};
可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。
另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题。有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。
下面介绍一个有限状态机的函数库Javascript Finite State Machine。这个库非常好懂,可以帮助我们加深理解,而且功能一点都不弱。
该库提供一个全局对象StateMachine,使用该对象的create方法,可以生成有限状态机的实例。
1
var fsm = StateMachine.create();
生成的时候,需要提供一个参数对象,用来描述实例的性质。比如,交通信号灯(红绿灯)可以这样描述:
1
2
3
4
5
6
7
8
9
10
11
12
var fsm = StateMachine.create({
initial: 'green',
events: [
{ name: 'warn', from: 'green', to: 'yellow' },
{ name: 'stop', from: 'yellow', to: 'red' },
{ name: 'ready', from: 'red', to: 'yellow' },
{ name: 'go', from: 'yellow', to: 'green' }
]
});
交通信号灯的初始状态(initial)为green,events属性是触发状态改变的各种事件,比如warn事件使得green状态变成yellow状态,stop事件使得yellow状态变成red状态等等。
生成实例以后,就可以随时查询当前状态。
- fsm.current :返回当前状态。
- fsm.is(s) :返回一个布尔值,表示状态s是否为当前状态。
- fsm.can(e) :返回一个布尔值,表示事件e是否能在当前状态触发。
- fsm.cannot(e) :返回一个布尔值,表示事件e是否不能在当前状态触发。
Javascript Finite State Machine允许为每个事件指定两个回调函数,以warn事件为例:
- onbeforewarn:在warn事件发生之前触发。
- onafterwarn(可简写成onwarn) :在warn事件发生之后触发。
同时,它也允许为每个状态指定两个回调函数,以green状态为例:
- onleavegreen :在离开green状态时触发。
- onentergreen(可简写成ongreen) :在进入green状态时触发。
假定warn事件使得状态从green变为yellow,上面四类回调函数的发生顺序如下:onbeforewarn → onleavegreen → onenteryellow → onafterwarn。
除了为每个事件和状态单独指定回调函数,还可以为所有的事件和状态指定通用的回调函数。
- onbeforeevent :任一事件发生之前触发。
- onleavestate :离开任一状态时触发。
- onenterstate :进入任一状态时触发。
- onafterevent :任一事件结束后触发。
如果事件的回调函数里面有异步操作(比如与服务器进行Ajax通信),这时我们可能希望等到异步操作结束,再发生状态改变。这就要用到transition方法。
1
2
3
4
5
6
7
fsm.onleavegreen = function(){
light.fadeOut('slow', function() {
fsm.transition();
});
return StateMachine.ASYNC;
};
上面代码的回调函数里面,有一个异步操作(light.fadeOut)。如果不希望状态立即改变,就要让回调函数返回StateMachine.ASYNC,表示状态暂时不改变;等到异步操作结束,再调用transition方法,使得状态发生改变。
Javascript Finite State Machine还允许指定错误处理函数,当发生了当前状态不可能发生的事件时自动触发。
1
2
3
4
5
6
7
var fsm = StateMachine.create({
// ...
error: function(eventName, from, to, args, errorCode, errorMessage) {
return 'event ' + eventName + ': ' + errorMessage;
},
// ...
});
比如,当前状态是green,理论上这时只可能发生warn事件。要是这时发生了stop事件,就会触发上面的错误处理函数。
Javascript Finite State Machine的基本用法就是上面这些,更详细的介绍可以参见它的主页。
原文来自:http://www.ruanyifeng.com/blog/2013/09/finite-state_machine_for_javascript.html
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/5b26e975.html b/posts/5b26e975.html
index 2b7642fd..8d9cbb50 100644
--- a/posts/5b26e975.html
+++ b/posts/5b26e975.html
@@ -1,4 +1,4 @@
-着色器之书11 - AIGISSS 着色器之书11
本文最后更新于:1 个月前

Noise 噪声
稍事休息,我们来转换一下思路。我们已经玩过了看起来像电视白噪音的 random 函数,尽管脑子里还在嗡嗡地转着 shader,但是已经眼花缭乱了。是时候出去走走了。
我们感知得到浮动在皮肤上的空气,晒在脸上的阳光。世界如此生动而丰富。颜色,质地,声音。当我们走在路上,不免会注意到路面、石头、树木和云朵的表面的样子。

这些纹理的不可预测性可以叫做“random”(随机),但是它们看起来不像是我们之前玩的 random。「真实世界」是如此的丰富而复杂!我们如何才能才能用计算机模拟这些多样的纹理呢?
这就是 Ken Perlin 想要解答的问题。在20世纪80年代早期,他被委任为电影 “Tron”(电子世界争霸战)制作现实中的纹理。为了解决这个问题,他想出了一个优雅的算法,且获得了奥斯卡奖(名副其实)。

下面这个并不是经典的 Perlin noise 算法,但是这是一个理解如何生成 noise 的好的出发点。
着色器之书11 - AIGISSS 着色器之书11
本文最后更新于:1 个月前

Noise 噪声
稍事休息,我们来转换一下思路。我们已经玩过了看起来像电视白噪音的 random 函数,尽管脑子里还在嗡嗡地转着 shader,但是已经眼花缭乱了。是时候出去走走了。
我们感知得到浮动在皮肤上的空气,晒在脸上的阳光。世界如此生动而丰富。颜色,质地,声音。当我们走在路上,不免会注意到路面、石头、树木和云朵的表面的样子。

这些纹理的不可预测性可以叫做“random”(随机),但是它们看起来不像是我们之前玩的 random。「真实世界」是如此的丰富而复杂!我们如何才能才能用计算机模拟这些多样的纹理呢?
这就是 Ken Perlin 想要解答的问题。在20世纪80年代早期,他被委任为电影 “Tron”(电子世界争霸战)制作现实中的纹理。为了解决这个问题,他想出了一个优雅的算法,且获得了奥斯卡奖(名副其实)。

下面这个并不是经典的 Perlin noise 算法,但是这是一个理解如何生成 noise 的好的出发点。
着色器之书15 - AIGISSS 着色器之书15
本文最后更新于:1 个月前
图像处理
贴图

Graphic cards (GPUs) have special memory types for images. Usually on CPUs images are stored as arrays of bytes but GPUs store images as sampler2D
which is more like a table (or matrix) of floating point vectors. More interestingly, the values of this table of vectors are continuous. That means that values between pixels are interpolated in a low level.
In order to use this feature we first need to upload the image from the CPU to the GPU, to then pass the id
of the texture to the right uniform
. All that happens outside the shader.
Once the texture is loaded and linked to a valid uniform sampler2D
you can ask for specific color value at specific coordinates (formated on a vec2
variable) using the texture2D()
function which will return a color formatted on a vec4
variable.
1
vec4 texture2D(sampler2D texture, vec2 coordinates)
Check the following code where we load Hokusai’s Wave (1830) as uniform sampler2D u_tex0
and we call every pixel of it inside the billboard:
If you pay attention you will note that the coordinates for the texture are normalized! What a surprise right? Textures coordinates are consistent with the rest of the things we had seen and their coordinates are between 0.0 and 1.0 which match perfectly with the normalized space coordinates we have been using.
Now that you have seen how we correctly load a texture, it is time to experiment to discover what we can do with it, by trying:
- Scaling the previous texture by half.
- Rotating the previous texture 90 degrees.
- Hooking the mouse position to the coordinates to move it.
Why you should be excited about textures? Well first of all forget about the sad 255 values for channel; once your image is transformed into a uniform sampler2D
you have all the values between 0.0 and 1.0 (depending on what you set the precision
to ). That’s why shaders can make really beautiful post-processing effects.
Second, the vec2()
means you can get values even between pixels. As we said before the textures are a continuum. This means that if you set up your texture correctly you can ask for values all around the surface of your image and the values will smoothly vary from pixel to pixel with no jumps!
Finally, you can set up your image to repeat in the edges, so if you give values over or lower of the normalized 0.0 and 1.0, the values will wrap around starting over.
All these features make your images more like an infinite spandex fabric. You can stretch and shrink your texture without noticing the grid of bytes they are originally composed of or the ends of it. To experience this take a look at the following code where we distort a texture using the noise function we already made.
纹理分辨率
Above examples play well with squared images, where both sides are equal and match our squared billboard. But for non-squared images things can be a little more tricky, and unfortunately centuries of pictorial art and photography found more pleasant to the eye non-squared proportions for images.

How we can solve this problem? Well we need to know the original proportions of the image to know how to stretch the texture correctly in order to have the original aspect ratio. For that the texture width and height are passed to the shader as an uniform
, which in our example framework are passed as an uniform vec2
with the same name of the texture followed with proposition Resolution
. Once we have this information on the shader we can get the aspect ratio by dividing the width
for the height
of the texture resolution. Finally by multiplying this ratio to the coordinates on y
we will shrink this axis to match the original proportions.
Uncomment line 21 of the following code to see this in action.
- What we need to do to center this image?
数码装饰

You may be thinking that this is unnecessarily complicated… and you are probably right. Also this way of working with images leaves enough room to different hacks and creative tricks. Try to imagine that you are an upholster and by stretching and folding a fabric over a structure you can create better and new patterns and techniques.

This level of craftsmanship links back to some of the first optical experiments ever made. For example on games sprite animations are very common, and is inevitably to see on it reminiscence to phenakistoscope, zoetrope and praxinoscope.
This could seem simple but the possibilities of modifying textures coordinates are enormous. For example:
Now is your turn:
Can you make a kaleidoscope using what we have learned?
Way before Oculus or google cardboard, stereoscopic photography was a big thing. Could you code a simple shader to re-use these beautiful images?
- What other optical toys can you re-create using textures?
In the next chapters we will learn how to do some image processing using shaders. You will note that finally the complexity of shader makes sense, because it was in a big sense designed to do this type of process. We will start doing some image operations!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书15 - AIGISSS 着色器之书15
本文最后更新于:1 个月前
图像处理
贴图

Graphic cards (GPUs) have special memory types for images. Usually on CPUs images are stored as arrays of bytes but GPUs store images as sampler2D
which is more like a table (or matrix) of floating point vectors. More interestingly, the values of this table of vectors are continuous. That means that values between pixels are interpolated in a low level.
In order to use this feature we first need to upload the image from the CPU to the GPU, to then pass the id
of the texture to the right uniform
. All that happens outside the shader.
Once the texture is loaded and linked to a valid uniform sampler2D
you can ask for specific color value at specific coordinates (formated on a vec2
variable) using the texture2D()
function which will return a color formatted on a vec4
variable.
1
vec4 texture2D(sampler2D texture, vec2 coordinates)
Check the following code where we load Hokusai’s Wave (1830) as uniform sampler2D u_tex0
and we call every pixel of it inside the billboard:
If you pay attention you will note that the coordinates for the texture are normalized! What a surprise right? Textures coordinates are consistent with the rest of the things we had seen and their coordinates are between 0.0 and 1.0 which match perfectly with the normalized space coordinates we have been using.
Now that you have seen how we correctly load a texture, it is time to experiment to discover what we can do with it, by trying:
- Scaling the previous texture by half.
- Rotating the previous texture 90 degrees.
- Hooking the mouse position to the coordinates to move it.
Why you should be excited about textures? Well first of all forget about the sad 255 values for channel; once your image is transformed into a uniform sampler2D
you have all the values between 0.0 and 1.0 (depending on what you set the precision
to ). That’s why shaders can make really beautiful post-processing effects.
Second, the vec2()
means you can get values even between pixels. As we said before the textures are a continuum. This means that if you set up your texture correctly you can ask for values all around the surface of your image and the values will smoothly vary from pixel to pixel with no jumps!
Finally, you can set up your image to repeat in the edges, so if you give values over or lower of the normalized 0.0 and 1.0, the values will wrap around starting over.
All these features make your images more like an infinite spandex fabric. You can stretch and shrink your texture without noticing the grid of bytes they are originally composed of or the ends of it. To experience this take a look at the following code where we distort a texture using the noise function we already made.
纹理分辨率
Above examples play well with squared images, where both sides are equal and match our squared billboard. But for non-squared images things can be a little more tricky, and unfortunately centuries of pictorial art and photography found more pleasant to the eye non-squared proportions for images.

How we can solve this problem? Well we need to know the original proportions of the image to know how to stretch the texture correctly in order to have the original aspect ratio. For that the texture width and height are passed to the shader as an uniform
, which in our example framework are passed as an uniform vec2
with the same name of the texture followed with proposition Resolution
. Once we have this information on the shader we can get the aspect ratio by dividing the width
for the height
of the texture resolution. Finally by multiplying this ratio to the coordinates on y
we will shrink this axis to match the original proportions.
Uncomment line 21 of the following code to see this in action.
- What we need to do to center this image?
数码装饰

You may be thinking that this is unnecessarily complicated… and you are probably right. Also this way of working with images leaves enough room to different hacks and creative tricks. Try to imagine that you are an upholster and by stretching and folding a fabric over a structure you can create better and new patterns and techniques.

This level of craftsmanship links back to some of the first optical experiments ever made. For example on games sprite animations are very common, and is inevitably to see on it reminiscence to phenakistoscope, zoetrope and praxinoscope.
This could seem simple but the possibilities of modifying textures coordinates are enormous. For example:
Now is your turn:
Can you make a kaleidoscope using what we have learned?
Way before Oculus or google cardboard, stereoscopic photography was a big thing. Could you code a simple shader to re-use these beautiful images?
- What other optical toys can you re-create using textures?
In the next chapters we will learn how to do some image processing using shaders. You will note that finally the complexity of shader makes sense, because it was in a big sense designed to do this type of process. We will start doing some image operations!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/64ddb0a.html b/posts/64ddb0a.html
index f8d06588..278b4fe7 100644
--- a/posts/64ddb0a.html
+++ b/posts/64ddb0a.html
@@ -1 +1 @@
-使用vscode在leetcode上提交代码 - AIGISSS 使用vscode在leetcode上提交代码
本文最后更新于:1 年前
前言
在浏览器上使用 leetcode 总觉得不是很方便,如果能和 vscode 结合,应该是如鱼得水、如虎添翼、珠联璧合。
事实上是在 vscode 的插件库中已经有了,直接搜索 leetcode,第一个出现的就是。

安装使用这个插件是有条件的,要有 node 环境

安装好了在左边就有一个 leetcode 的图标, 登入成功后刷新列表,即可查看题目。效果如下:

如果没有账号,就要去力扣(LeetCode)官网或者LeetCode注册账号!!
初步的可以在// @lc code=end
下面结合Code Runner
得到初步验证

更多用例的测试需要点Test
来验证。
可以使用Submit
来看看自己的代码的排名情况。
可以去讨论一下或者看看别人的的解决方法。提交到 github,然后就可以到处做题目了!!

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+使用vscode在leetcode上提交代码 - AIGISSS 使用vscode在leetcode上提交代码
本文最后更新于:1 年前
前言
在浏览器上使用 leetcode 总觉得不是很方便,如果能和 vscode 结合,应该是如鱼得水、如虎添翼、珠联璧合。
事实上是在 vscode 的插件库中已经有了,直接搜索 leetcode,第一个出现的就是。

安装使用这个插件是有条件的,要有 node 环境

安装好了在左边就有一个 leetcode 的图标, 登入成功后刷新列表,即可查看题目。效果如下:

如果没有账号,就要去力扣(LeetCode)官网或者LeetCode注册账号!!
初步的可以在// @lc code=end
下面结合Code Runner
得到初步验证

更多用例的测试需要点Test
来验证。
可以使用Submit
来看看自己的代码的排名情况。
可以去讨论一下或者看看别人的的解决方法。提交到 github,然后就可以到处做题目了!!

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/6e7939e6.html b/posts/6e7939e6.html
index b6b19fd9..8ae8817d 100644
--- a/posts/6e7939e6.html
+++ b/posts/6e7939e6.html
@@ -1 +1 @@
-webpack学习与总结 - AIGISSS webpack学习与总结
本文最后更新于:1 年前
初识 Webpack
webpack 是模块打包工具,把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码,包括如下内容。
- 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
- 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
- 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
- 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
- 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
- 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
- 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。 构建给前端开发注入了更大的活力,解放了我们的生产力。
历史上先后出现一系列构建工具,它们各有其优缺点。由于前端工程师很熟悉 JavaScript ,Node.js 又可以胜任所有构建需求,所以大多数构建工具都是用 Node.js 开发的。下面来一一介绍它们。
Npm Script
Npm Script 是一个任务执行者。Npm 是在安装 Node.js 时附带的包管理器,Npm Script 则是 Npm 内置的一个功能,允许在 package.json
文件里面使用 scripts
字段定义任务:
1
2
3
4
5
6
{
"scripts": {
"dev": "node dev.js",
"pub": "node build.js"
}
}
里面的 scripts
字段是一个对象,每个属性对应一段 Shell 脚本,以上代码定义了两个任务 dev
和 pub
。 其底层实现原理是通过调用 Shell 去运行脚本命令,例如执行 npm run pub
命令等同于执行命令 node build.js
。
Npm Script的优点是内置,无须安装其他依赖。其缺点是功能太简单,虽然提供了 pre
和 post
两个钩子,但不能方便地管理多个任务之间的依赖。
Grunt
Grunt 和 Npm Script 类似,也是一个任务执行者。Grunt 有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.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
module.exports = function(grunt) {
// 所有插件的配置信息
grunt.initConfig({
// uglify 插件的配置信息
uglify: {
app_task: {
files: {
'build/app.min.js': ['lib/index.js', 'lib/test.js']
}
}
},
// watch 插件的配置信息
watch: {
another: {
files: ['lib/*.js'],
}
}
});
// 告诉 grunt 我们将使用这些插件
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// 告诉grunt当我们在终端中启动 grunt 时需要执行哪些任务
grunt.registerTask('dev', ['uglify','watch']);
};
在项目根目录下执行命令 grunt dev
就会启动 JavaScript 文件压缩和自动刷新功能。
Grunt的优点是:
- 灵活,它只负责执行你定义的任务;
- 大量的可复用插件封装好了常见的构建任务。
Grunt的缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用。
Grunt 相当于进化版的 Npm Script,它的诞生其实是为了弥补 Npm Script 的不足。
Gulp
Gulp 是一个基于流的自动化构建工具。 除了可以管理和执行任务,还支持监听文件、读写文件。Gulp 被设计得非常简单,只通过下面5个方法就可以胜任几乎所有构建场景:
- 通过
gulp.task
注册一个任务; - 通过
gulp.run
执行任务; - 通过
gulp.watch
监听文件变化; - 通过
gulp.src
读取文件; - 通过
gulp.dest
写文件。
Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递,大致使用如下:
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
// 引入 Gulp
var gulp = require('gulp');
// 引入插件
var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
// 编译 SCSS 任务
gulp.task('sass', function() {
// 读取文件通过管道喂给插件
gulp.src('./scss/*.scss')
// SCSS 插件把 scss 文件编译成 CSS 文件
.pipe(sass())
// 输出文件
.pipe(gulp.dest('./css'));
});
// 合并压缩 JS
gulp.task('scripts', function() {
gulp.src('./js/*.js')
.pipe(concat('all.js'))
.pipe(uglify())
.pipe(gulp.dest('./dist'));
});
// 监听文件变化
gulp.task('watch', function(){
// 当 scss 文件被编辑时执行 SCSS 任务
gulp.watch('./scss/*.scss', ['sass']);
gulp.watch('./js/*.js', ['scripts']);
});
Gulp 的优点是好用又不失灵活,既可以单独完成构建也可以和其它工具搭配使用。其缺点是和 Grunt 类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。
可以将Gulp 看作 Grunt 的加强版。相对于 Grunt,Gulp增加了监听文件、读写文件、流式处理的功能。
Webpack
Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。
其官网的首页图很形象的画出了 Webpack 是什么,如下:

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。
Webpack 具有很大的灵活性,能配置如何处理文件,大致使用如下:
1
2
3
4
5
6
7
8
module.exports = {
// 所有模块的入口,Webpack 从入口开始递归解析出所有依赖的模块
entry: './app.js',
output: {
// 把入口所依赖的所有模块打包成一个文件 bundle.js 输出
filename: 'bundle.js'
}
}
Webpack的优点是:
- 专注于处理模块化的项目,能做到开箱即用一步到位;
- 通过 Plugin 扩展,完整好用又不失灵活;
- 使用场景不仅限于 Web 开发;
- 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
- 良好的开发体验。
Webpack的缺点是只能用于采用模块化开发的项目。
为什么选择 Webpack
上面介绍的构建工具是按照它们诞生的时间排序的,它们是时代的产物,侧面反映出 Web 开发的发展趋势如下:
- 在 Npm Script 和 Grunt 时代,Web 开发要做的事情变多,流程复杂,自动化思想被引入,用于简化流程;
- 在 Gulp 时代开始出现一些新语言用于提高开发效率,流式处理思想的出现是为了简化文件转换的流程,例如将 ES6 转换成 ES5。
- 在 Webpack 时代由于单页应用的流行,一个网页的功能和实现代码变得庞大,Web 开发向模块化改进。
这些构建工具都有各自的定位和专注点,它们之间既可以单独地完成任务,也可以相互搭配起来弥补各自的不足。 在了解这些常见的构建工具后,你需要根据自己的需求去判断应该如何选择和搭配它们才能更好地完成自己的需求。
经过多年的发展, Webpack 已经成为构建工具中的首选,这是有原因的:
- 大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack 可以为这些新项目提供一站式的解决方案;
- Webpack 有良好的生态链和维护团队,能提供良好的开发体验和保证质量;
- Webpack 被全世界的大量 Web 开发者使用和验证,能找到各个层面所需的教程和经验分享。
使用Webpack
安装 Webpack 到本项目
在开始给项目加入构建前, 在安装 Webpack 前请确保你的系统安装了5.0.0及以上版本的 Node.js。你还需要先新建一个 Web 项目,进入项目根目录执行 npm init
来初始化最简单的采用了模块化开发的项目;
要安装 Webpack 到本项目,可按照你的需要选择以下任意命令运行:
1
2
3
4
5
6
7
8
9
# npm i -D 是 npm install --save-dev 的简写,是指安装模块并保存到 package.json 的 devDependencies
# 安装最新稳定版
npm i -D webpack
# 安装指定版本
npm i -D webpack@<version>
# 安装最新体验版本
npm i -D webpack@beta
安装完后你可以通过这些途径运行安装到本项目的 Webpack:
在项目根目录下对应的命令行里通过 node_modules/.bin/webpack
运行 Webpack 可执行文件。
在 Npm Script 里定义的任务会优先使用本项目下的 Webpack,代码如下:
1
2
3
"scripts": {
"start": "webpack --config webpack.config.js"
}
可以使用npx webpack
Node 自带 npm 模块,所以可以直接使用 npx 命令。万一不能用,就要手动安装一下。
1
npm install -g npx
npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了 webpack 而且全局没有安装 webpack。
1
npm install -D webpack
一般来说,调用 webpack,只能在项目脚本和 package.json 的scripts
字段里面, 如果想在命令行下调用,必须像下面这样。
1
2
3
# 项目的根目录下执行
cd node_modules\.bin
webpack --version # 输出 4.41.0
npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。
1
2
# 项目的根目录下执行
npx webpack --version
npx 的原理很简单,就是运行的时候,会到node_modules/.bin
路径和环境变量$PATH
里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH
,所以系统命令也可以调用。
除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app
这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
1
$ npx create-react-app my-react-app
上面代码运行时,npx 将create-react-app
下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app
。
下载全局模块时,npx 允许指定版本。
1
$ npx uglify-js@3.1.0 main.js -o ./dist/main.js
上面代码指定使用 3.1.0 版本的uglify-js
压缩脚本。
注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server
模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。
1
$ npx http-server
参考:npx 使用教程
安装 Webpack 到全局
安装到全局后你可以在任何地方共用一个 Webpack 可执行文件,而不用各个项目重复安装,安装方式如下:
1
npm i -g webpack
虽然介绍了以上两种安装方式,但是我们推荐安装到本项目,原因是可防止不同项目依赖不同版本的 Webpack 而导致冲突。使用 Webpack
默认配置
Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js
文件读取配置,所以你还需要新建它,其内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path');
module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
}
};
由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object
对象。
使用loader
Webpack 把一切文件看作模块,CSS 文件也不例外,Webpack 不原生支持解析 CSS 文件。要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制。Webpack的配置修改使用如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
// 用正则去匹配要用该 loader 转换的 CSS 文件
test: /\.css$/,
use: ['style-loader', 'css-loader?minimize'],
}
]
}
};
Loader 可以看作具有文件转换功能的翻译员,配置里的 module.rules
数组配置了一组规则,告诉 Webpack 在遇到哪些文件时使用哪些 Loader 去加载和转换。 如上配置告诉 Webpack 在遇到以 .css
结尾的文件时先使用 css-loader
读取 CSS 文件,再交给 style-loader
把 CSS 内容注入到 JavaScript 里。 在配置 Loader 时需要注意的是:
use
属性的值需要是一个由 Loader 名称组成的数组,Loader 的执行顺序是由后到前的;- 每一个 Loader 都可以通过 URL querystring 的方式传入参数,例如
css-loader?minimize
中的 minimize
告诉 css-loader
要开启 CSS 压缩。
Webpack 构建前要先安装新引入的 Loader:
1
npm i -D style-loader css-loader
给 Loader 传入属性的方式除了有 querystring 外,还可以通过 Object 传入,以上的 Loader 配置可以修改为如下:
1
2
3
4
5
6
7
8
9
use: [
'style-loader',
{
loader:'css-loader',
options:{
minimize:true,
}
}
]
除了在 webpack.config.js
配置文件中配置 Loader 外,还可以在源码中指定用什么 Loader 去处理文件。 以加载 CSS 文件为例,修改上面例子中的 main.js
如下:
1
require('style-loader!css-loader?minimize!./main.css');
这样就能指定对 ./main.css
这个文件先采用 css-loader 再采用 style-loader 转换。
使用 Plugin
plugin 可以在 webpack 运行到某一时刻的时候,帮你做一些事情。
htmlWepackPluginnn 会在打包结束后,自动生成一个 html 文件,并把打包的生成的 js 自动引入到这个 html 文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const HtmlWebpackPlugin = require("html-webpack-plugin"); //通过 npm 安装
const webpack = require("webpack"); //访问内置的插件
const path = require("path");
const config = {
mode: "production",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: "file-loader"
}
]
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })]
};
module.exports = config;
使用 DevServer
DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 Webpack ,并接收 Webpack 发出的文件更变信号,通过 WebSocket 协议自动刷新网页做到实时预览。
首先需要安装 DevServer:
1
npm i -D webpack-dev-server
安装成功后执行 webpack-dev-server
命令, DevServer 就启动了,这时你会看到控制台有一串日志输出:
1
2
Project is running at http://localhost:8080/
webpack output is served from /
这意味着 DevServer 启动的 HTTP 服务器监听在 http://localhost:8080/
,DevServer 启动后会一直驻留在后台保持运行,访问这个网址你就能获取项目根目录下的 index.html
。 用浏览器打开这个地址你会发现页面空白错误原因是 ./dist/bundle.js
加载404了。 同时你会发现并没有文件输出到 dist
目录,原因是 DevServer 会把 Webpack 构建出的文件保存在内存中,在要访问输出的文件时,必须通过 HTTP 服务访问。 由于 DevServer 不会理会 webpack.config.js
里配置的 output.path
属性,所以要获取 bundle.js
的正确 URL 是 http://localhost:8080/bundle.js
,对应的 index.html
应该修改为:
1
2
3
4
5
6
7
8
9
10
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入 DevServer 输出的 JavaScript 文件-->
<script src="bundle.js"></script>
</body>
</html>
Webpack 可以在启动 Webpack 时通过 webpack --watch
来开启监听模式,实时预览。Webpack 支持生成 Source Map,只需在启动时带上 --devtool source-map
参数。 加上参数重启 DevServer 后刷新页面,再打开 Chrome 浏览器的开发者工具,就可在 Sources 栏中看到可调试的源代码了。
DevServer 还有一种被称作模块热替换的刷新技术。 模块热替换能做到在不重新加载整个网页的情况下,通过将被更新过的模块替换老的模块,再重新执行一次来实现实时预览。 模块热替换相对于默认的刷新机制能提供更快的响应和更好的开发体验。 模块热替换默认是关闭的,要开启模块热替换,你只需在启动 DevServer 时带上 --hot
参数,重启 DevServer 后再去更新文件就能体验到模块热替换的神奇了。
1
2
3
const config=require('./webpack.config.js');
const complier=wepack(config)
// 在node中使用webpack的方法。
核心概念
Webpack 有以下几个核心概念。
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
- Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。
Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。
webpack的配置
Entry
entry 是配置模块的入口,可抽象成输入, Webpack 执行构建的第 步将从入 口开始,搜寻及递归解析出所有入口依赖的模块。entry 配置是必填的,若不填则将导致 Webpack 报错、退出。
context
Webpack 在寻找相对路径的文件时会以 context 为根目录, context 默认为执行启动
Webpack 时所在的当前工作目录。如果想改变 context 的默认配置,则可以在配置文件里这
样设置它
1
2
3
module.exports = {
context: path.resolve(__dirname,'app')
}
loader
file-loader
就会拷贝到打包文件夹内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
mode: "development",
entry: {
main: "./src/index.js"
},
module: {
rules: [
{
test: /\.jpg$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]"
}
}
}
]
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
}
};
url-loader
会变成 base64,在 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
const path = require("path");
module.exports = {
mode: "development",
entry: {
main: "./src/index.js"
},
module: {
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
loader: "url-loader",
options: {
name: "[name].[ext]",
outputPath: "images/",
limit: 1024
}
}
}
]
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
}
};
css-loader style-loader
Webpack 进阶
Webpack 配置及案例
Webpack 原理及脚手架
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+webpack学习与总结 - AIGISSS webpack学习与总结
本文最后更新于:1 年前
初识 Webpack
webpack 是模块打包工具,把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码,包括如下内容。
- 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
- 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
- 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
- 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
- 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
- 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
- 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。 构建给前端开发注入了更大的活力,解放了我们的生产力。
历史上先后出现一系列构建工具,它们各有其优缺点。由于前端工程师很熟悉 JavaScript ,Node.js 又可以胜任所有构建需求,所以大多数构建工具都是用 Node.js 开发的。下面来一一介绍它们。
Npm Script
Npm Script 是一个任务执行者。Npm 是在安装 Node.js 时附带的包管理器,Npm Script 则是 Npm 内置的一个功能,允许在 package.json
文件里面使用 scripts
字段定义任务:
1
2
3
4
5
6
{
"scripts": {
"dev": "node dev.js",
"pub": "node build.js"
}
}
里面的 scripts
字段是一个对象,每个属性对应一段 Shell 脚本,以上代码定义了两个任务 dev
和 pub
。 其底层实现原理是通过调用 Shell 去运行脚本命令,例如执行 npm run pub
命令等同于执行命令 node build.js
。
Npm Script的优点是内置,无须安装其他依赖。其缺点是功能太简单,虽然提供了 pre
和 post
两个钩子,但不能方便地管理多个任务之间的依赖。
Grunt
Grunt 和 Npm Script 类似,也是一个任务执行者。Grunt 有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.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
module.exports = function(grunt) {
// 所有插件的配置信息
grunt.initConfig({
// uglify 插件的配置信息
uglify: {
app_task: {
files: {
'build/app.min.js': ['lib/index.js', 'lib/test.js']
}
}
},
// watch 插件的配置信息
watch: {
another: {
files: ['lib/*.js'],
}
}
});
// 告诉 grunt 我们将使用这些插件
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// 告诉grunt当我们在终端中启动 grunt 时需要执行哪些任务
grunt.registerTask('dev', ['uglify','watch']);
};
在项目根目录下执行命令 grunt dev
就会启动 JavaScript 文件压缩和自动刷新功能。
Grunt的优点是:
- 灵活,它只负责执行你定义的任务;
- 大量的可复用插件封装好了常见的构建任务。
Grunt的缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用。
Grunt 相当于进化版的 Npm Script,它的诞生其实是为了弥补 Npm Script 的不足。
Gulp
Gulp 是一个基于流的自动化构建工具。 除了可以管理和执行任务,还支持监听文件、读写文件。Gulp 被设计得非常简单,只通过下面5个方法就可以胜任几乎所有构建场景:
- 通过
gulp.task
注册一个任务; - 通过
gulp.run
执行任务; - 通过
gulp.watch
监听文件变化; - 通过
gulp.src
读取文件; - 通过
gulp.dest
写文件。
Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递,大致使用如下:
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
// 引入 Gulp
var gulp = require('gulp');
// 引入插件
var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
// 编译 SCSS 任务
gulp.task('sass', function() {
// 读取文件通过管道喂给插件
gulp.src('./scss/*.scss')
// SCSS 插件把 scss 文件编译成 CSS 文件
.pipe(sass())
// 输出文件
.pipe(gulp.dest('./css'));
});
// 合并压缩 JS
gulp.task('scripts', function() {
gulp.src('./js/*.js')
.pipe(concat('all.js'))
.pipe(uglify())
.pipe(gulp.dest('./dist'));
});
// 监听文件变化
gulp.task('watch', function(){
// 当 scss 文件被编辑时执行 SCSS 任务
gulp.watch('./scss/*.scss', ['sass']);
gulp.watch('./js/*.js', ['scripts']);
});
Gulp 的优点是好用又不失灵活,既可以单独完成构建也可以和其它工具搭配使用。其缺点是和 Grunt 类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。
可以将Gulp 看作 Grunt 的加强版。相对于 Grunt,Gulp增加了监听文件、读写文件、流式处理的功能。
Webpack
Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。
其官网的首页图很形象的画出了 Webpack 是什么,如下:

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。
Webpack 具有很大的灵活性,能配置如何处理文件,大致使用如下:
1
2
3
4
5
6
7
8
module.exports = {
// 所有模块的入口,Webpack 从入口开始递归解析出所有依赖的模块
entry: './app.js',
output: {
// 把入口所依赖的所有模块打包成一个文件 bundle.js 输出
filename: 'bundle.js'
}
}
Webpack的优点是:
- 专注于处理模块化的项目,能做到开箱即用一步到位;
- 通过 Plugin 扩展,完整好用又不失灵活;
- 使用场景不仅限于 Web 开发;
- 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
- 良好的开发体验。
Webpack的缺点是只能用于采用模块化开发的项目。
为什么选择 Webpack
上面介绍的构建工具是按照它们诞生的时间排序的,它们是时代的产物,侧面反映出 Web 开发的发展趋势如下:
- 在 Npm Script 和 Grunt 时代,Web 开发要做的事情变多,流程复杂,自动化思想被引入,用于简化流程;
- 在 Gulp 时代开始出现一些新语言用于提高开发效率,流式处理思想的出现是为了简化文件转换的流程,例如将 ES6 转换成 ES5。
- 在 Webpack 时代由于单页应用的流行,一个网页的功能和实现代码变得庞大,Web 开发向模块化改进。
这些构建工具都有各自的定位和专注点,它们之间既可以单独地完成任务,也可以相互搭配起来弥补各自的不足。 在了解这些常见的构建工具后,你需要根据自己的需求去判断应该如何选择和搭配它们才能更好地完成自己的需求。
经过多年的发展, Webpack 已经成为构建工具中的首选,这是有原因的:
- 大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack 可以为这些新项目提供一站式的解决方案;
- Webpack 有良好的生态链和维护团队,能提供良好的开发体验和保证质量;
- Webpack 被全世界的大量 Web 开发者使用和验证,能找到各个层面所需的教程和经验分享。
使用Webpack
安装 Webpack 到本项目
在开始给项目加入构建前, 在安装 Webpack 前请确保你的系统安装了5.0.0及以上版本的 Node.js。你还需要先新建一个 Web 项目,进入项目根目录执行 npm init
来初始化最简单的采用了模块化开发的项目;
要安装 Webpack 到本项目,可按照你的需要选择以下任意命令运行:
1
2
3
4
5
6
7
8
9
# npm i -D 是 npm install --save-dev 的简写,是指安装模块并保存到 package.json 的 devDependencies
# 安装最新稳定版
npm i -D webpack
# 安装指定版本
npm i -D webpack@<version>
# 安装最新体验版本
npm i -D webpack@beta
安装完后你可以通过这些途径运行安装到本项目的 Webpack:
在项目根目录下对应的命令行里通过 node_modules/.bin/webpack
运行 Webpack 可执行文件。
在 Npm Script 里定义的任务会优先使用本项目下的 Webpack,代码如下:
1
2
3
"scripts": {
"start": "webpack --config webpack.config.js"
}
可以使用npx webpack
Node 自带 npm 模块,所以可以直接使用 npx 命令。万一不能用,就要手动安装一下。
1
npm install -g npx
npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了 webpack 而且全局没有安装 webpack。
1
npm install -D webpack
一般来说,调用 webpack,只能在项目脚本和 package.json 的scripts
字段里面, 如果想在命令行下调用,必须像下面这样。
1
2
3
# 项目的根目录下执行
cd node_modules\.bin
webpack --version # 输出 4.41.0
npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。
1
2
# 项目的根目录下执行
npx webpack --version
npx 的原理很简单,就是运行的时候,会到node_modules/.bin
路径和环境变量$PATH
里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH
,所以系统命令也可以调用。
除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app
这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
1
$ npx create-react-app my-react-app
上面代码运行时,npx 将create-react-app
下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app
。
下载全局模块时,npx 允许指定版本。
1
$ npx uglify-js@3.1.0 main.js -o ./dist/main.js
上面代码指定使用 3.1.0 版本的uglify-js
压缩脚本。
注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server
模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。
1
$ npx http-server
参考:npx 使用教程
安装 Webpack 到全局
安装到全局后你可以在任何地方共用一个 Webpack 可执行文件,而不用各个项目重复安装,安装方式如下:
1
npm i -g webpack
虽然介绍了以上两种安装方式,但是我们推荐安装到本项目,原因是可防止不同项目依赖不同版本的 Webpack 而导致冲突。使用 Webpack
默认配置
Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js
文件读取配置,所以你还需要新建它,其内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path');
module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
}
};
由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object
对象。
使用loader
Webpack 把一切文件看作模块,CSS 文件也不例外,Webpack 不原生支持解析 CSS 文件。要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制。Webpack的配置修改使用如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
// 用正则去匹配要用该 loader 转换的 CSS 文件
test: /\.css$/,
use: ['style-loader', 'css-loader?minimize'],
}
]
}
};
Loader 可以看作具有文件转换功能的翻译员,配置里的 module.rules
数组配置了一组规则,告诉 Webpack 在遇到哪些文件时使用哪些 Loader 去加载和转换。 如上配置告诉 Webpack 在遇到以 .css
结尾的文件时先使用 css-loader
读取 CSS 文件,再交给 style-loader
把 CSS 内容注入到 JavaScript 里。 在配置 Loader 时需要注意的是:
use
属性的值需要是一个由 Loader 名称组成的数组,Loader 的执行顺序是由后到前的;- 每一个 Loader 都可以通过 URL querystring 的方式传入参数,例如
css-loader?minimize
中的 minimize
告诉 css-loader
要开启 CSS 压缩。
Webpack 构建前要先安装新引入的 Loader:
1
npm i -D style-loader css-loader
给 Loader 传入属性的方式除了有 querystring 外,还可以通过 Object 传入,以上的 Loader 配置可以修改为如下:
1
2
3
4
5
6
7
8
9
use: [
'style-loader',
{
loader:'css-loader',
options:{
minimize:true,
}
}
]
除了在 webpack.config.js
配置文件中配置 Loader 外,还可以在源码中指定用什么 Loader 去处理文件。 以加载 CSS 文件为例,修改上面例子中的 main.js
如下:
1
require('style-loader!css-loader?minimize!./main.css');
这样就能指定对 ./main.css
这个文件先采用 css-loader 再采用 style-loader 转换。
使用 Plugin
plugin 可以在 webpack 运行到某一时刻的时候,帮你做一些事情。
htmlWepackPluginnn 会在打包结束后,自动生成一个 html 文件,并把打包的生成的 js 自动引入到这个 html 文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const HtmlWebpackPlugin = require("html-webpack-plugin"); //通过 npm 安装
const webpack = require("webpack"); //访问内置的插件
const path = require("path");
const config = {
mode: "production",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: "file-loader"
}
]
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })]
};
module.exports = config;
使用 DevServer
DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 Webpack ,并接收 Webpack 发出的文件更变信号,通过 WebSocket 协议自动刷新网页做到实时预览。
首先需要安装 DevServer:
1
npm i -D webpack-dev-server
安装成功后执行 webpack-dev-server
命令, DevServer 就启动了,这时你会看到控制台有一串日志输出:
1
2
Project is running at http://localhost:8080/
webpack output is served from /
这意味着 DevServer 启动的 HTTP 服务器监听在 http://localhost:8080/
,DevServer 启动后会一直驻留在后台保持运行,访问这个网址你就能获取项目根目录下的 index.html
。 用浏览器打开这个地址你会发现页面空白错误原因是 ./dist/bundle.js
加载404了。 同时你会发现并没有文件输出到 dist
目录,原因是 DevServer 会把 Webpack 构建出的文件保存在内存中,在要访问输出的文件时,必须通过 HTTP 服务访问。 由于 DevServer 不会理会 webpack.config.js
里配置的 output.path
属性,所以要获取 bundle.js
的正确 URL 是 http://localhost:8080/bundle.js
,对应的 index.html
应该修改为:
1
2
3
4
5
6
7
8
9
10
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入 DevServer 输出的 JavaScript 文件-->
<script src="bundle.js"></script>
</body>
</html>
Webpack 可以在启动 Webpack 时通过 webpack --watch
来开启监听模式,实时预览。Webpack 支持生成 Source Map,只需在启动时带上 --devtool source-map
参数。 加上参数重启 DevServer 后刷新页面,再打开 Chrome 浏览器的开发者工具,就可在 Sources 栏中看到可调试的源代码了。
DevServer 还有一种被称作模块热替换的刷新技术。 模块热替换能做到在不重新加载整个网页的情况下,通过将被更新过的模块替换老的模块,再重新执行一次来实现实时预览。 模块热替换相对于默认的刷新机制能提供更快的响应和更好的开发体验。 模块热替换默认是关闭的,要开启模块热替换,你只需在启动 DevServer 时带上 --hot
参数,重启 DevServer 后再去更新文件就能体验到模块热替换的神奇了。
1
2
3
const config=require('./webpack.config.js');
const complier=wepack(config)
// 在node中使用webpack的方法。
核心概念
Webpack 有以下几个核心概念。
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
- Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。
Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。
webpack的配置
Entry
entry 是配置模块的入口,可抽象成输入, Webpack 执行构建的第 步将从入 口开始,搜寻及递归解析出所有入口依赖的模块。entry 配置是必填的,若不填则将导致 Webpack 报错、退出。
context
Webpack 在寻找相对路径的文件时会以 context 为根目录, context 默认为执行启动
Webpack 时所在的当前工作目录。如果想改变 context 的默认配置,则可以在配置文件里这
样设置它
1
2
3
module.exports = {
context: path.resolve(__dirname,'app')
}
loader
file-loader
就会拷贝到打包文件夹内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
mode: "development",
entry: {
main: "./src/index.js"
},
module: {
rules: [
{
test: /\.jpg$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]"
}
}
}
]
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
}
};
url-loader
会变成 base64,在 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
const path = require("path");
module.exports = {
mode: "development",
entry: {
main: "./src/index.js"
},
module: {
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
loader: "url-loader",
options: {
name: "[name].[ext]",
outputPath: "images/",
limit: 1024
}
}
}
]
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
}
};
css-loader style-loader
Webpack 进阶
Webpack 配置及案例
Webpack 原理及脚手架
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/87ed0839.html b/posts/87ed0839.html
index 105ec649..e205e710 100644
--- a/posts/87ed0839.html
+++ b/posts/87ed0839.html
@@ -1 +1 @@
-VSCode中python代码输出中文乱码解决方法 - AIGISSS VSCode中python代码输出中文乱码解决方法
本文最后更新于:1 年前
在 vscode 中编写 python 代码,输出中文时,控制台输出为乱码解决方法:
先检查右下角编码集设置是否正确

修改完后运行仍不行,可以在”文件”-”首选项”-”用户设置”中搜索 code-runner.executorMap 选项,提示需要在 setting.json 中修改

在 json 中添加下列属性
1
2
3
"code-runner.executorMap": {
"python": "set PYTHONIOENCODING=utf8 && python -u"
}
https://mp.weixin.qq.com/s/QIJ-QHkxZUyKyQAPG49vPg
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+VSCode中python代码输出中文乱码解决方法 - AIGISSS VSCode中python代码输出中文乱码解决方法
本文最后更新于:1 年前
在 vscode 中编写 python 代码,输出中文时,控制台输出为乱码解决方法:
先检查右下角编码集设置是否正确

修改完后运行仍不行,可以在”文件”-”首选项”-”用户设置”中搜索 code-runner.executorMap 选项,提示需要在 setting.json 中修改

在 json 中添加下列属性
1
2
3
"code-runner.executorMap": {
"python": "set PYTHONIOENCODING=utf8 && python -u"
}
https://mp.weixin.qq.com/s/QIJ-QHkxZUyKyQAPG49vPg
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/89cc5dea.html b/posts/89cc5dea.html
index 995bae8c..f5659c8d 100644
--- a/posts/89cc5dea.html
+++ b/posts/89cc5dea.html
@@ -1 +1 @@
-Threejs学习和总结基础篇 - AIGISSS Threejs学习和总结基础篇
本文最后更新于:1 年前
概述
使用 Three.js 显示创建的内容,我们必须需要的三大件是:渲染器、相机和场景。相机获取到场景内显示的内容,然后再通过渲染器渲染到画布上面。
要在屏幕上展示3D图形,思路大体上都是这样的:
- 构建一个三维空间
- Three中称之为场景(Scene)
- 选择一个观察点,并确定观察方向/角度等
- Three中称之为相机(Camera)
- 在场景中添加供观察的物体
- Three中的物体有很多种,包括Mesh,Line,Points等,它们都继承自Object3D类
- 将观察到的场景渲染到屏幕上的指定区域
- Three中使用Renderer完成这一工作
拿电影来类比的话,场景对应于整个布景空间,相机是拍摄镜头,渲染器用来把拍摄好的场景转换成胶卷。场景 Scene
场景是所有物体的容器,也对应着我们创建的三维世界。场景允许你设置哪些对象被Three.js渲染以及渲染在哪里。在场景中放置对象、灯光和相机。
相机 Camera
1
2
3
4
5
// 初始化相机
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); // 实例化相机
camera.position.set(0, 0, 15);
}
相机
Three中的相机有两种,分别是正投影相机THREE.OrthographicCamera和透视投影相机THREE.PerspectiveCamera。

正交投影与透视投影的区别如上图所示,左图是正交投影,物体发出的光平行地投射到屏幕上,远近的方块都是一样大的;右图是透视投影,近大远小,符合我们平时看东西的感觉。
维基百科:三维投影
正交投影相机

可以近似地认为,视景体里的物体平行投影到近平面上,然后近平面上的图像被渲染到屏幕上。
透视投影相机

坐标系
Camera是三维世界中的观察者,为了观察这个世界,首先我们要描述空间中的位置,Three中使用采用常见的右手坐标系定位:

我们这里使用到的是 THREE.PerspectiveCamera
,这个相机模拟人眼看到的效果,就是具有透视的效果,近大远小。
第一行,我们实例化了一个透视相机,需要四个值,分别是视野、宽高比、近裁面和远裁面。
- 视野:当前相机视野的宽度,值越大,渲染出来的内容也会更多。
- 宽高比:默认是按照画布显示的宽高比例来设置,如果比例设置的不对,会发现渲染出来的画面有拉伸或者压缩的感觉。
- 近裁面和远裁面:这个是设置相机可以看到的场景内容的范围,如果场景内的内容位置不在这两个值内的话,将不会被显示到渲染的画面中。
第二行,我们设置了相机的位置。
渲染器 Renderer
1
2
3
4
5
6
//初始化渲染器
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高
document.body.appendChild(renderer.domElement); //添加到dom
}
第一行实例化了一个 THREE.WebGLRenderer
,这是一个基于 WebGL 渲染的渲染器,当然,Three.js 向下兼容,还有 CanvasRenderer、CSS2DRenderer、CSS3DRenderer 和 SVGRenderer,这四个渲染器分别基于 canvas2D、CSS2D、CSS3D 和 SVG 渲染的渲染器。由于,作为 3D 渲染,WebGL 渲染的效果最好,并且支持的功能更多。
第二行,调用了一个设置函数 setSize 方法,这个是设置需要显示的窗口大小。案例是基于浏览器全屏显示,所以设置了浏览器窗口的宽和高。
第三行,renderer.domElement
是在实例化渲染器时生成的一个 Canvas 画布,渲染器渲染界面生成的内容,都将在这个画布上显示。所以,我们将这个画布添加到了 DOM 当中,来显示渲染的内容。
模型 Object3D
渲染器,场景和相机都全了,是不是就能显示东西了?不能!因为场景内没有内容,即使渲染出来也是一片漆黑,所以我们需要往场景里面添加内容。接下来,我们将查看 initMesh 方法,看看如何创建一个最简单的模型:
1
2
3
4
5
6
7
8
//创建模型
function initMesh() {
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
}
创建一个网格(模型)需要两种对象:几何体和材质。
- 几何体代表模型的形状,它是由固定的点的位置组成,点绘制出面,面组成了模型。
- 材质是我们看到当前模型显示出来的效果,如显示的颜色,质感等。
Three中供显示的物体有很多,它们都继承自Object3D类
Mesh
我们都知道,计算机的世界里,一条弧线是由有限个点构成的有限条线段连接得到的。线段很多时,看起来就是一条平滑的弧线了。
计算机中的三维模型也是类似的,普遍的做法是用三角形组成的网格来描述,我们把这种模型称之为Mesh模型。

这是那只著名的斯坦福兔子。它在3D图形中的地位与数字图像处理领域中著名的Lenna是类似的。
看这只兔子,随着三角形数量的增加,它的表面越来越平滑
在Three中,Mesh的构造函数是这样的:Mesh( geometry, material )
geometry是它的形状,material是它的材质。
不止是Mesh,创建很多物体都要用到这两个属性。下面我们来看看这两个重要的属性。Material和Geometry是相辅相成的,必须结合使用。
Geometry
Geometry,形状,相当直观。Geometry通过存储模型用到的点集和点间关系(哪些点构成一个三角形)来达到描述物体形状的目的。
Three提供了立方体(其实是长方体)、平面(其实是长方形)、球体、圆形、圆柱、圆台等许多基本形状;
你也可以通过自己定义每个点的位置来构造形状;
对于比较复杂的形状,我们还可以通过外部的模型文件导入。
Material
Material,材质,这就没有形状那么直观了。
材质其实是物体表面除了形状以为所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度。
这里讲一下材质(Material)、贴图(Map)和纹理(Texture)的关系。
材质上面已经提到了,它包括了贴图以及其它。
贴图其实是‘贴’和‘图’,它包括了图片和图片应当贴到什么位置。
纹理嘛,其实就是‘图’了。
Three提供了多种材质可供选择,能够自由地选择漫反射/镜面反射等材质。
光影Light
神说:要有光!
光影效果是让画面丰富的重要因素。
Three提供了包括环境光AmbientLight、点光源PointLight、 聚光灯SpotLight、方向光DirectionalLight、半球光HemisphereLight等多种光源。
只要在场景中添加需要的光源就好了。
让场景动起来
动画,就是多幅图片一直切换便可显示动画的效果。为了能显示动画的效果,我们首先要了解一个函数 requestAnimationFrame,这个函数专门为了动画而出现。它与 setInterval 相比,优势在于不需要设置多长时间重新渲染,而是在当前线程内 JS 空闲时自动渲染,并且最大帧数控制在一秒60帧。所以,我们书写了一个可以循环调用的函数:
1
2
3
4
function animate() {
requestAnimationFrame(animate); //循环调用函数
// ...
}
在循环调用的函数中,每一帧我们都让页面重新渲染相机拍摄下来的内容:
1
renderer.render( scene, camera ); //渲染界面
渲染的 render 方法需要两个值,第一个值是场景对象,第二个值是相机对象。这意味着,你可以有多个相机和多个场景,可以通过渲染不同的场景和相机让画布上显示不同的画面。
但是,如果现在一直渲染的话,我们发现就一个立方体在那,也没有动,我们需要做的是让立方体动起来:
1
2
mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度
mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度
每一个实例化的网格对象都有一个 rotation 的值,通过设置这个值可以让立方体旋转起来。在每一帧里,我们让立方体沿 x 轴方向旋转0.01弧度,沿 y 轴旋转0.02弧度(1π 弧度等于180度角度)。
Threejs性能检测插件
在 Three.js 里面,遇到最多的问题就是性能问题,所以我们需要时刻检测当前的 Three.js 的性能。现在 Three.js 常使用的一款插件叫 stats。接下来我们看看如何将 stats 插件在 Three.js 的项目中使用。
- 首先在页面中引入插件代码:
1
<script src="http://www.wjceo.com/lib/js/libs/stats.min.js"></script>
这是 一个 CDN 的地址,直接引入即可。
- 然后,我们需要实例化一个 stats 对象,然后把对象内生成的 DOM 添加到页面当中。
1
2
stats = new Stats();
document.body.appendChild(stats.dom);
- 最后一步,我们需要在 requestAnimationFrame 的回调里面更新每次渲染的时间:
1
2
3
4
5
function animate() {
requestAnimationFrame(animate); //循环调用函数
stats.update(); //更新性能插件
renderer.render( scene, camera ); //渲染界面
}
使用了性能检测插件以后,整个代码如下:
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Stats插件案例</title>
<style>
body {
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
</head>
<body onload="init()">
<script src="https://cdn.bootcss.com/three.js/92/three.js"></script>
<script src="http://www.wjceo.com/lib/js/libs/stats.min.js"></script>
<script>
//声明一些全局变量
var renderer, camera, scene, geometry, material, mesh, stats;
//初始化渲染器
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高
document.body.appendChild(renderer.domElement); //添加到dom
}
//初始化场景
function initScene() {
scene = new THREE.Scene(); //实例化场景
}
//初始化相机
function initCamera() {
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
); //实例化相机
camera.position.set(0, 0, 15);
}
//创建模型
function initMesh() {
geometry = new THREE.BoxGeometry(2, 2, 2); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh(geometry, material); //创建网格
scene.add(mesh); //将网格添加到场景
}
//运行动画
function animate() {
requestAnimationFrame(animate); //循环调用函数
mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度
mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度
stats.update(); //更新性能检测框
renderer.render(scene, camera); //渲染界面
}
//性能检测框
function initStats() {
stats = new Stats();
document.body.appendChild(stats.dom);
}
//初始化函数,页面加载完成是调用
function init() {
initRenderer();
initScene();
initCamera();
initMesh();
initStats();
animate();
}
</script>
</body>
</html>
场景Scene
场景是我们每个 Three.js 项目里面放置内容的容器,我们也可以拥有多个场景进行切换展示,你可以在场景内放置你的模型、灯光和照相机。还可以通过调整场景的位置,让场景内的所有内容都一起跟着调整位置。

场景的结构
之前在刚刚开始学 JavaScript 基础时,我们总免不了去操作 DOM 对象,而且我们都知道 DOM 的结构是树形结构的,Three.js 也遵循了这样的理念,将所有可以添加到场景内的结构梳理成了一种树形的结构,方便我们能够更好的理解Three.js。
我们可以把 Scene 想象成一个 body,body 内可以添加 DOM 对象,scene 内也可以添加它的 3D 对象,这样一层层的嵌套出来,组成了我们现在需要的项目。所以,在 Three.js 中,为了方便操作,将所有 3D 对象共同的内容抽象成了一个基类,就是 THREE.Object3D
。
THREE.Object3D
为了方便操作,Three.js 将每个能够直接添加到场景内的对象都继承自一个基类——THREE.Object3D
,以后我们将继承自这个基类的对象称为 3D 对象,判断一个对象是否是继承自 THREE.Object3D
,我们可以这么做:
1
2
obj instanceof THREE.Object3D
//继承至返回 true 否则返回false
这个基类上封装了我们常用的一些方法,下面我们分别介绍下。
添加一个 3D 对象
1
scene.add(mesh); //将网格添加到场景
将一个立方体添加到场景内显示。
这个方法不光能够在场景内使用,而且也可以将一个 3D 对象添加到另一个 3D 对象里面,代码如下:
1
parent.add(child);
获取一个 3D 对象
获取一个 3D 对象可以使用 getObjectByName 通过 3D 对象的 name 值进行获取,在获取前我们首先要设置当前 3D 对象的 name 值:
1
2
3
object3D.name = "firstObj";
scene.add(object3D);
scene.getObjectByName("firstObj"); //返回第一个匹配的3d对象
另一种方式就是使用 getObjectById 通过 3D 对象的 id 值进行获取,3D 对象的 id 值只能读取,它是在添加到场景时,按 1、2、3、4、5……的顺序默认生成的一个值,无法自定义:
1
scene.getObjectById(1); //返回id值为1的3d对象
删除一个 3D 对象
如果我们想隐藏一个 3D 对象,而不让它显示,可以通过设置它的 visible的值来实现:
1
mesh.visible = false; //设置为false,模型将不会被渲染到场景内
如果一个模型不再被使用到,需要彻底删除,我们可以使用 remove 方法进行删除:
1
2
scene.add(mesh); //将一个模型添加到场景当中
scene.remove(mesh); //将一个模型从场景中删除
获取到所有的子类
每一个 3D 对象都有一个 children 属性,这是一个数组,里面包含所有添加的 3D 对象:
1
2
3
4
scene.add(mesh1);
scene.add(mesh2);
console.log(scene.children);
// [mesh1, mesh2]
如果想获取 3D 对象下面所有的 3D 对象,我们可以通过 traverse方法获取:
1
2
3
4
5
6
mesh1.add(mesh2); //mesh2是mesh1的子元素
scene.add(mesh1); //mesh1是场景对象的子元素
scene.traverse(fucntion(child){
console.log(child);
});
//将按顺序分别将mesh1和mesh2打印出来
获取 3D 对象的父元素
每个 3D 对象都有一个父元素,可以通过 parent 属性进行获取:
1
2
scene.add(mesh); //将模型添加到场景
console.log(mesh.parent === scene); //true
修改 3D 对象
前面介绍了场景的结构以及场景的 3D 对象添删查,下面,我们接着介绍对场景内模型的一些操作。
修改位置方式
我们可以通过设置模型的 position 属性来修改模型的当前位置,具体方法有以下几种。
- 单独设置每个方向的属性。
1
2
3
mesh.position.x = 3; //将模型的位置调整到x正轴距离原点为3的位置。
mesh.position.y += 5; //将模型的y轴位置以当前的位置向上移动5个单位。
mesh.position.z -= 6;
- 直接一次性设置所有方向的属性。
1
mesh.position.set(3, 5, -6); //直接将模型的位置设置在x轴为3,y轴为5,z轴为-6的位置
- Three.js 模型的位置属性是一个
THREE.Vector3
(三维向量)的对象,我们可以直接重新赋值一个新的对象。
1
mesh.position = new THREE.Vector3(3, 5, -6); //上面的设置位置也可以通过这样设置。
修改大小的方式
模型导入后,很多情况下都需要调整模型的大小。我们可以通过设置模型的 scale 属性来调整大小。
- 第一种方式是单独设置每个方向的缩放。
1
2
3
mesh.scale.x = 2; //模型沿x轴放大一倍
mesh.scale.y = 0.5; //模型沿y轴缩小一倍
mesh.scale.z = 1; //模型沿z轴保持不变
- 第二种是使用 set 方法。
1
2
mesh.scale.set(2, 2, 2); //每个方向等比放大一倍
mesh.scale.set(0.5, 0.5, 0.5); //每个方向等比缩小一倍
- 第三种方式,由于 scale 属性也是一个三维向量,我们可以通过赋值的方式重新修改。
1
mesh.scale = new THREE.Vector3(2, 2, 2); //每个方向都放大一倍
修改模型的转向
很多情况下,我们需要对模型进行旋转,以达到将模型显示出它需要显示的方位,我们可以通过设置模型的 rotation 属性进行旋转(注意:旋转 Three.js 使用的是弧度不是角度)。
- 第一种方式是单独设置每个轴的旋转。
1
2
3
mesh.rotation.x = Math.PI; //模型沿x旋转180度
mesh.rotation.y = Math.PI * 2; //模型沿y轴旋转360度,跟没旋转一样的效果。。。
mesh.rotation.z = - Math.PI / 2; //模型沿z轴逆时针旋转90度
- 第二种方式就是使用 set 方法重新赋值。
1
mesh.rotation.set(Math.PI, 0, - Math.PI / 2); //旋转效果和第一种显示的效果相同
正常模型的旋转方式是按照 XYZ 依次旋转的,如果你想先旋转其他轴,我们可以添加第四项修改,有可能的情况为:YZX、ZXY、XZY、YXZ 和 ZYX。
1
mesh.rotation.set(Math.PI, 0, - Math.PI / 2, "YZX"); //先沿y轴旋转180度,再沿z轴旋转0度,最后沿x轴逆时针旋转90度
- 第三种方式,模型的 rotation 属性其实是一个欧拉角对象(
THREE.Euler
),欧拉角后面会讲解到,我们可以通过重新赋值一个欧拉角对象来实现旋转调整:
1
mesh.rotation = new THREE.Euler(Math.PI, 0, - Math.PI / 2, "YZX");
使用 dat.GUI 实现页面调试
有些时候,我们需要调整模型的位置或者大小等,且需要每次都去场景内调试,一种常用的插件 dat.GUI
- 首先,需要将插件的源码引入到页面当中,我这里直接使用 CDN 的连接。
1
<script src="https://cdn.bootcss.com/dat-gui/0.7.1/dat.gui.min.js"></script>
- 创建一个对象,在里面设置我们需要修改的一些数据。
1
2
3
4
5
controls = {
positionX:0,
positionY:0,
positionZ:0
};
- 实例化
dat.GUI
对象,将需要修改的配置添加对象中,并监听变化回调。
1
2
3
4
5
6
7
8
gui = new dat.GUI();
gui.add(controls, "positionX", -1, 1).onChange(updatePosition);
gui.add(controls, "positionY", -1, 1).onChange(updatePosition);
gui.add(controls, "positionZ", -1, 1).onChange(updatePosition);
function updatePosition() {
mesh.position.set(controls.positionX, controls.positionY, controls.positionZ);
}

这样,只要我们每次修改对象里面的值,都会触发 updatePosition
回调,来更新模型的位置。这样,我们就实现了一个简单的案例。
接下来,我列出一下经常会使用到一些方式和方法。
生成一个输入框
dat.GUI
能够根据 controls 值的不同而生成不同的操作方法,如果值的类型为字符串或者数字类型,则可以生成一个默认的输入框:
1
2
3
gui.add(controls, "positionX");
gui.add(controls, "positionY");
gui.add(controls, "positionZ");

生成一个可以滑动的滑块
使用 gui.add()
方法,如果值为数字类型,传入的第三个值(最小值)和第四个值(最大值),就限制了值能够取值的范围,这样就生成了可以滑动的滑块:
1
2
3
gui.add(controls, "positionX", -1, 1); //设置了最小值和最大值,可以生成滑块
gui.add(controls, "positionY").max(1); //只设置了最大值,无法生成滑块
gui.add(controls, "positionZ").min(-1); //只设置了最小值,也无法生成滑块

我们还可以通过 step() 方法来限制每次变化的最小值,也就是你增加或者减少,必须都是这个值的倍数:
1
2
3
gui.add(controls, "positionX", -10, 10).step(1); //限制必须为整数
gui.add(controls, "positionY", -10, 10).step(0.1); //每次变化都是0.1的倍数
gui.add(controls, "positionZ", -10, 10).step(10); //每次变化都是10的倍数

生成一个下拉框
只要按规则在 gui.add()
的第三个值传入一个对象或者数组,dat.GUI
就能够自动匹配生成一个下拉框:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:0,
positionY:false,
positionZ:"middle"
};
gui = new dat.GUI();
gui.add(controls, "positionX", {left:-10, middle:0, right:10}); //数字类型的下拉框
gui.add(controls, "positionY", [true, false]); //布尔值类型的下拉框
gui.add(controls, "positionZ", ["left", "middle", "right"]); //字符串类型的下拉框

生成一个 Checkbox
只要 controls 的值是一个布尔类型,使用 gui.add()
方法就可以生成一个复选框:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:true,
positionY:false,
positionZ:false
};
gui = new dat.GUI();
gui.add(controls, "positionX");
gui.add(controls, "positionY");
gui.add(controls, "positionZ");

生成一个点击事件按钮
如果 controls 的值为一个函数 Function,dat.GUI
会自动生成一个可以点击的按钮,当按下时就会触发这个函数事件:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:function () {},
positionY:function () {},
positionZ:function () {}
};
gui = new dat.GUI();
gui.add(controls, "positionX");
gui.add(controls, "positionY");
gui.add(controls, "positionZ");

修改显示的名称
我们可以在后面使用 name() 事件设置显示的名称:
1
2
3
gui.add(controls, "positionX", -1, 1).name("x轴");
gui.add(controls, "positionY", -1, 1).name("y轴");
gui.add(controls, "positionZ", -1, 1).name("z轴");

颜色选择框
实现颜色选择框,首先需要一种正常格式的颜色值,比如 CSS 的颜色样式或者 RGB 格式,然后再使用 gui.addColor()
的方法添加:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:"#cccccc", //css样式
positionY: [0, 255, 255], //RGB格式
positionZ: [0, 255, 255, 0.6] //RGBA格式
};
gui = new dat.GUI();
gui.addColor(controls, "positionX").name("x轴");
gui.addColor(controls, "positionY").name("y轴");
gui.addColor(controls, "positionZ").name("z轴");

监听事件回调
dat.GUI
给我们提供了监听事件回调的方法 onChange(),如果值变化就能够触发函数回调:
1
2
3
4
5
6
7
gui.add(controls, "positionX", -1, 1).onChange(updatePosition);
gui.add(controls, "positionY", -1, 1).onChange(updatePosition);
gui.add(controls, "positionZ", -1, 1).onChange(updatePosition);
function updatePosition() {
mesh.position.set(controls.positionX, controls.positionY, controls.positionZ);
}
创建分组
我们可以使用 gui.addFolder()
方法来创建分组:
1
2
3
4
5
6
7
8
gui = new dat.GUI();
var first = gui.addFolder("第一个分组"); //创建第一个分组
first.add(controls, "positionX", -1, 1).onChange(updatePosition);
first.open();
var two = gui.addFolder("第二个分组");
two.add(controls, "positionY", -1, 1).onChange(updatePosition);
two.add(controls, "positionZ", -1, 1).onChange(updatePosition);

几何体Geometry
一个模型是由几何体 Geometry 和材质 Material 组成。Three.js 内置了很多的几何体种类,如立方体、三棱锥、球、八面体、十二面体、二十面体等等,将介绍这些几何体的模型创建和几何体的通用方法。

Geometry 和 BufferGeometry
当前 Three.js 内置了这两种几何体类型,这两个几何体类型都用于存储模型的顶点位置、面的索引、法向量、颜色、UV 纹理以及一些自定义的属性。
它们两个的区别是:BufferGeometry 存储的都是一些原始的数据,性能比 Geometry 高,很适合存储一些放入场景内不需要再额外操作的模型。而 Geometry 的优势刚好相反,Geometry 比 BufferGeometry 更友好,使用了 Three.js 提供的 THREE.Vector3
或者 THREE.Color
这样的对象来存储数据(顶点位置、面、颜色等),这些对象易于阅读和编辑,但效率低于 BufferGeometry 使用的类型化数组。
所以,我们可以根据项目的大小来使用不同的几何体,小项目可以使用 Geometry 实现,中大型的项目还是推荐 BufferGeometry。
我们将使用较为简单的 Geometry 来实现案例。
Geometry 和 BufferGeometry 互转
在这里插一点内容,两种几何体类型可以互转,所以,不要担心现在使用的是哪种。
- Geometry 转换成 BufferGeometry
转换代码,如下:
1
2
3
4
5
//实例化一个Geometry对象
var geo = new THREE.Geometry();
//调用对象的fromBufferGeometry方法,并将需要转换的geometry传入
var bufferGeo = geo.fromBufferGeometry(geometry);
//返回的对象转换成的BufferGeometry
BufferGeometry
转换成Geometry
1
2
3
4
5
//实例化一个BufferGeometry对象
var bufferGeo = new THREE.BufferGeometry();
//调用对象的fromGeometry方法,并将需要转换的bufferGeometry传入
var geo = bufferGeo.fromGeometry(bufferGeometry);
//返回的对象转换成的Geometry
接下来,我们将讲解一下 Three.js 内置几何体。
立方体 BoxGeometry 和 BoxBufferGeometry
立方体是最早接触的几何体,可以通过设置长宽高来创建各种各样的立方体。
看下面的案例,代码:
1
2
3
4
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );
案例中的构造函数:
1
BoxGeometry(width : '浮点类型', height : '浮点类型', depth : '浮点类型', widthSegments : '整数类型', heightSegments : '整数类型', depthSegments : '整数类型')
各参数的含义:
- width:沿 X 轴的宽度,默认值为1;
- height:沿 Y 轴的高度,默认值为1;
- depth:沿 Z 轴的深度,默认值为1;
- widthSegments:可选,沿着边的宽度的分割面的数量。默认值为1;
- heightSegments:可选,沿着边的高度的分割面的数量。默认值为1;
- depthSegments:可选,沿着边的深度的分割面的数量。缺省值是1;
案例查看:请点击这里。
圆 CircleGeometry 和 CircleBufferGeometry
在 WebGL 里,所有的模型都是通过三角形面组成,圆形是由多个三角形分段构成,这些三角形分段围绕一个中心点延伸并且延伸到给定半径以外。它从起始角度和给定的中心角度逆时针方向构建。它也可以用来创建规则的多边形,其中线段的数量决定了边的数量。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.CircleGeometry( 5, 32 );
var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
var circle = new THREE.Mesh( geometry, material );
scene.add( circle );
案例中的构造函数,如下:
1
CircleGeometry(radius : '浮点类型', segments : '整数类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数的含义:
- radius:圆的半径,默认值为1;
- segments:段数(三角形),最小值为3,默认值为8;
- thetaStart:第一段的起始角度,默认值为0;
- thetaLength:圆形扇形的中心角,通常称为 theta。默认值是 2 * Pi,画出一个整圆。
案例查看:请点击这里。
圆锥 ConeGeometry 和 ConeBufferGeometry
这是一个可以创建圆锥体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.ConeGeometry( 5, 20, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var cone = new THREE.Mesh( geometry, material );
scene.add( cone );
案例中的构造函数,如下:
1
ConeGeometry(radius : '浮点类型', height : '浮点类型', radialSegments : '整数类型', heightSegments : '整数类型', openEnded : '布尔类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数含义:
- radius:底部圆锥的半径,默认值为1;
- height:圆锥体的高度,默认值为1;
- radialSegments:圆锥周围的分段面数,默认值为8;
- heightSegments:沿圆锥体高度的面的行数,默认值为1;
- openEnded:圆锥体底部是是隐藏还是显示,默认值为 false,显示;
- thetaStart:第一段的起始角度,默认值是0(Three.js 的0度位置)
- thetaLength — 圆形扇形的中心角,通常称为 theta。默认值是2 * Pi,画出一个整圆。
案例查看:请点击这里。
圆柱 CylinderGeometry 和 CylinderBufferGeometry
这是一个可以创建圆柱几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.CylinderGeometry( 5, 5, 20, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var cylinder = new THREE.Mesh( geometry, material );
scene.add( cylinder );
案例中的构造函数,如下:
1
CylinderGeometry(radiusTop : '浮点类型', radiusBottom : '浮点类型', height : '浮点类型', radialSegments : '整数类型', heightSegments : '整数类型', openEnded : '布尔类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数含义:
- radiusTop:顶部圆柱体的半径。默认值为1;
- radiusBottom:底部圆柱体的半径。默认值为1;
- height:圆柱体的高度。默认值为1;
- radialSegments:圆柱周围的分段面数。默认值为8;
- heightSegments:沿圆柱体高度的面的行数。默认值为1;
- openEnded:圆柱体的两端是否显示,默认值是 false,显示;
- thetaStart:第一段的起始角度,默认值是0(Three.js 的0度位置)。
- thetaLength — 圆形扇形的中心角,通常称为 theta。默认值是2 * Pi,画出一个整圆。
案例查看:请点击这里。
球 SphereGeometry 和 SphereBufferGeometry
这是一个可以创建球体几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.SphereGeometry( 5, 32, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var sphere = new THREE.Mesh( geometry, material );
scene.add( sphere );
案例中的构造函数,如下 :
1
SphereGeometry(radius : '浮点类型', widthSegments : '整数类型', heightSegments : '整数类型', phiStart : '浮点类型', phiLength : '浮点类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数含义:
- radius:球体半径。默认值是1;
- widthSegments:水平线段的数量。最小值是3,默认值是8;
- heightSegments:垂直段的数量。最小值是2,默认值是6;
- phiStart:指定水平渲染起始角度。默认值为0;
- phiLength:指定水平渲染角度大小。默认值是 Math.PI * 2;
- thetaStart:指定垂直渲染起始角度。默认值为0;
- thetaLength:指定垂直渲染角度大小。默认是 Math.PI。
默认是渲染整个的圆,如果线段越多,显得球体越圆滑。
案例查看:请点击这里。
平面 PlaneGeometry 和 SphereBufferGeometry
这是一个可以创建平面几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.PlaneGeometry( 5, 20, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
var plane = new THREE.Mesh( geometry, material );
scene.add( plane );
案例中的构造函数,如下 :
1
PlaneGeometry(width : '浮点类型', height : '浮点类型', widthSegments : '整数类型', heightSegments : '整数类型')
各参数含义:
- width:沿 X 轴的宽度。默认值为1;
- height:沿着 Y 轴的高度。默认值为1;
- widthSegments:宽度的分段数,可选。默认值为1;
- heightSegments:高度的分段数,可选。默认值为1。
案例查看:请点击这里。
圆环 TorusGeometry 和 TorusBufferGeometry
一个可以创建圆环几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.TorusGeometry( 10, 3, 16, 100 );
var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
var torus = new THREE.Mesh( geometry, material );
scene.add( torus );
案例中的构造函数,如下 :
1
TorusGeometry(radius : '浮点类型', tube : '浮点类型', radialSegments : '整数类型', tubularSegments : '整数类型', arc : '浮点类型')
各参数含义:
- radius:圆环的半径,从圆环的中心到管的中心。默认值为1;
- tube:管的半径。默认值是0.4;
- radialSegments:横向分段数,默认值是8;
- tubularSegments:纵向分段数,默认值是6;
- arc — 绘制的弧度。默认值是 Math.PI * 2,绘制整个圆环。
案例查看:请点击这里。
以上是 Three.js 内置的一些基础的几何体,Three.js 还内置了一些其他的几何体模型(如字体几何体、拉伸几何体、车床几何体等),由于篇幅原因和难度原因,在这里不一一讲解。
如果迫不及待得想了解这一块的内容可以通过官方文档或者我的博客中关于 Three.js 的笔记来了解更多。
常用方法
Geometry 和 BufferGeomety 内置了一些常用的方法,在每一种几何体上面,我们都可以调用相关的方法来达到我们的目的。
center()
此方法为居中方法,可以根据边界框居中几何图形。
computeBoundingBox()
此方法可以计算几何的边界框,方法调用后,会更新 Geometry.boundingBox
属性,我们可以通过 Geometry.boundingBox
属性获取到一个包围几何体的立方体的每个轴向的最大值和最小值。
dispose()
将几何体从内存中删除,这个方法必须记得使用。如果频繁的删除模型,一定要记得将几何体从内存中删除掉。
总结案例
最后,我将上面介绍的所有几何体放到一个场景内,制作了一个小案例,案例代码地址如下:
材质Material
模型的表现,也就是我们看到的的模型外观——材质。

简单的说,就是物体看起来是什么质地。材质可以看成是材料和质感的结合。在渲染程序中,它是表面各种可视属性的结合,这些可视属性是指表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等。Three.js 给我们封装好了大部分的材质效果,避免我们使用复杂的 Shader 语言自己去实现。接下来我们先介绍下 Material 常用的一些属性和方法。
基本属性和方法
needsUpdate
如果修改了 Material 内的内容,需要将 needsUpdate 属性设置为 true,Three.js 会在下一帧里将修改内容同步到 WebGL 的显存内。切记不要在 requestAnimationFrame 方法内更新,会浪费性能,只需要在更新 Material 属性后设置一次即可。
side
此属性可以定义当前面的哪个方向会被渲染,默认值是 THREE.FrontSide
(只渲染正面),可选值有:THREE.BackSide
(只渲染背面)和 THREE.DoubleSide
(正面和背面都会渲染)。
transparent
此属性定义了材质是否可以透明,因为对于透明需要材质进行特殊处理,并在不透明的物体渲染完成后再渲染透明物体。当设置此属性为true
后,可以通过设置opacity
来调整透明度,默认为false
。
opacity
此属性可以定义材质的透明度,必须将材质的 transparent 设置为 true 才可使透明度管用。取值范围为 0.0 到 1.0。默认值是1.0,也就是不透明。
map
此属性可以配置当前材质的纹理贴图,是一个 THREE.Texture
对象,下面我们将会讲解如何给材质贴图。这是大部分材质都会有的属性,只有极其个别的材质如 LineBasicMaterial
(线材质)等没有这个属性。
wireframe
是否将模型渲染成线框,默认为 false。个别材质也没有这个属性。
dispose()
此方法用于将材质从内存中删除,在不需要使用当前材质时使用,但不会将材质的纹理贴图删除,如果需要将纹理贴图也删除,需要调用 material.map.dispose()
。
配置纹理贴图
由于经常使用纹理贴图,所以在这里单独讲解一下如何实现一个纹理贴图。 实现纹理贴图有以下两种方式。
第一种,使用 THREE.TextureLoader
进行生成纹理对象:
1
2
var texture = new THREE.TextureLoader().load( "textures/water.jpg" );
material.map = texture; //将纹理赋值给材质
或者直接实例化:
1
2
var texture = new THREE.Texture(canvas); //实例化的第一个对象可以是`img`、`canvas`和`video`。
material.map = texture; //将纹理赋值给材质
纹理重复问题
如果图片不是标准的2的幂数(2、4、8、16、32、64、128、256、512、1024、2048……),在控制台会给我们提示:“THREE.WebGLRenderer: image is not power of two”,意思就是说图片不是标准格式高宽不是2的幂数。我们需要的水平方向和垂直方向上设置的图片重复显示。需要配置的两个属性是:texture.wrapS
(水平方向重复)和 texture.wrapT
(垂直方向重复),默认值是:THREE.ClampToEdgeWrapping
,即纹理的最后一个像素延伸到网格的边缘。可选项有:THREE.RepeatWrapping
,表示纹理将重复无穷大;MirroredRepeatWrapping
,表示镜像重复,可以理解为重复时,反着绘制一个然后正着绘制一个,达到的效果就是没有强烈的过渡感觉。
needsUpdate 属性
如果更新了纹理的相关属性,需要将此属性设置为 true,将数据同步到 WebGL。
repeat
纹理在整个表面水平方向和垂直方向重复多少次,也会受纹理重复设置的影响,设置方式为:
1
2
3
4
var texture = new THREE.TextureLoader().load( "textures/water.jpg" );
texture.wrapS = THREE.RepeatWrapping; //设置水平方向无限循环
texture.wrapT = THREE.RepeatWrapping; //设置垂直方向无限循环
texture.repeat.set( 4, 4 ); //水平方向和垂直方向都重复四次
内置常用材质
在讲解常用材质之前,我们先讲解一下如何实例化一个材质和一些需要注意的地方。我们使用第一个讲到的材质 MeshBasicMaterial 作为例子。
MeshBasicMaterial 和设置颜色的方法
这种材质是一种简单的材质,不会受到光的影响,直接看到的效果就是整个物体的颜色都是一样,没有立体的感觉。在实例化材质时,我们可以传入一个对象,设置材质的相关属性可以通过对象属性的方式传入,但是属性 color(颜色)例外,实例化的时候可以传入十六进制数,也可以写十六进制字符串。实例化完成后再修改需要重新赋值 THREE.Color
对象,或者调用 material.color.set
方法赋值。
1
2
3
4
5
var material = new THREE.MeshBasicMaterial({color:0x00ffff});
var geometry = new THREE.BoxGeometry(1, 1, 1);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
上面的案例就是使用 MeshBasicMaterial 材质创建了一个立方体,我们设置了显示颜色为一种浅蓝色,除了上面实例化的时候进行设置,后面也可以再修改:
1
2
var material = new THREE.MeshBasicMaterial({color:0x00ffff}); //设置初始的颜色为浅蓝色
material.color.set(0xff00ff); //将颜色修改为紫色
我们也可以直接赋值一个新的 THREE.Color
对象,如:
1
2
var material = new THREE.MeshBasicMaterial({color:0x00ffff}); //设置初始的颜色为浅蓝色
material.color = new THREE.Color(0xff00ff); //将颜色修改为紫色
我们可以通过 new THREE.Color
创建一个颜色对象,Three.js 支持的颜色书写方式比较丰富,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//直接传入十六进制数或者字符串
var color = new THREE.Color( 0xff0000 );
var color = new THREE.Color( "#ff0000" );
//RGB 字符串
var color = new THREE.Color("rgb(255, 0, 0)");
var color = new THREE.Color("rgb(100%, 0%, 0%)");
//支持一百四十多中颜色名称
var color = new THREE.Color( 'skyblue' );
//HSL 字符串
var color = new THREE.Color("hsl(0, 100%, 50%)");
//支持RGB值设置在0到1之间的方式
var color = new THREE.Color( 1, 0, 0 );
MeshNormalMaterial 法向材质
这种材质会根据面的方向不同自动改变颜色,也是我们之前一直在用的材质。此材质不受灯光影响。
1
2
3
4
5
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
LineBasicMaterial 线条材质
在上一篇我们讲几何体时,没有讲解如何画直线,是由于直线需要单独的材质进行实现,所以我们将直线放到了材质这一篇中进行讲解。注意,由于 Windows 系统的原因,线的宽度只能为1。
要绘制线段,我们需要确定两个点,就是起点和终点,案例中我们使用了四个顶点创建了三条线。然后 Geometry 对象使用这组顶点配置几何体,实例化线的材质,最后使用 THREE.Line
生成线。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//添加直线
var pointsArr = [
new THREE.Vector3( -10, 0, -5 ),
new THREE.Vector3( -5, 15, 5 ),
new THREE.Vector3( 20, 15, -5 ),
new THREE.Vector3( 10, 0, 5 )
];
var lineGeometry = new THREE.Geometry(); //实例化几何体
lineGeometry.setFromPoints(pointsArr); //使用当前点的属性配置几何体
var lineMaterial = new THREE.LineBasicMaterial({color:0x00ff00}); //材质
line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line);
LineDashedMaterial 虚线
我们也可以创建虚线,这里我们来点新花样,就是实现曲线。曲线也和直线一样,在 Windows 系统线的粗度只能为1。
要创建曲线,我们需要使用到 THREE.CatmullRomCurve3
来生成一个 curve 对象,这是一个曲线对象,可以从对象获取生成的曲线的点的集合,在这里科普一下,曲线也是由无数段的直线组成的,段数分的越清晰,曲线过渡越顺滑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var pointsArr = [
new THREE.Vector3( -10, 0, -5 ),
new THREE.Vector3( -5, 15, 5 ),
new THREE.Vector3( 20, 15, -5 ),
new THREE.Vector3( 10, 0, 5 )
];
//指定一些用于生成曲线线的三维顶点
var curve = new THREE.CatmullRomCurve3(pointsArr);
var points = curve.getPoints( 50 ); //使用getPoints获取当前曲线分成50段后的所有顶点
var curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); //使用顶点生成几何体
var curveMaterial = new THREE.LineDashedMaterial( { color : 0xff0000 } ); //创建一条红色的线材质
// 使用THREE.Line创建线
curveLine = new THREE.Line( curveGeometry, curveMaterial );
curveLine.computeLineDistances(); //需要重新计算位置才能显示出虚线
scene.add(curveLine);
添加光
由于 MeshBasicMaterial 不会受光的影响,即使有光也不会影响它的效果,前面我们也没有添加光。但是后面介绍的材质会受到光源的影响,在介绍之前,我们需要添加一个光源,来影响材质的显示效果。
1
2
3
4
5
6
7
8
9
//创建灯光
function initLight() {
var light = new THREE.DirectionalLight(0xffffff); //添加了一个白色的平行光
light.position.set(20, 50, 50); //设置光的方向
scene.add(light); //添加到场景
//添加一个全局环境光
scene.add(new THREE.AmbientLight(0x222222));
}
上面我们添加了一个模拟太阳光线的平行光和一个对每一个物理都造成影响的环境光,具体的内容将会在下一篇讲解。
下面介绍的材质都是对光有反应的,而且如果场景内没有光,模型将无法显示。
MeshLambertMaterial 兰伯特材质
这种材质会对光有反应,但是不会出现高光,可以模拟一些粗糙的材质的物体,比如木头或者石头。
实现案例,如下:
1
2
3
4
5
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshLambertMaterial({color:0x00ffff}); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
MeshPhongMaterial 高光材质
这种材质具有高光效果,可以模拟一些光滑的物体的材质效果,比如油漆面,瓷瓦等光滑物体。
实现案例如下:
1
2
3
4
5
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshPhongMaterial({color:0x00ffff}); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
MeshStandardMaterial 基于物理的渲染(PBR)材质
这种材质基于物理的渲染(PBR)材质,生成的材质效果更佳,但是相应也占用更多的计算量。这种材质我们可以定义它的粗糙度来确定反光效果,经常用于模拟金属的质感,使金属质感更加真实。
实现案例如下:
1
2
3
4
5
6
7
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshPhongMaterial({color:0x00ffff}); //创建材质
material.metalness = 0.1; //设置的值的范围为0-1,值越小,材质越光滑,高光越明显
material.metalnessMap = 0.1; //设置的值的范围为0-1,值越大,越有生锈金属的质感,值越小反光越清晰
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
案例代码
相机Camera
相机是 Three.js 抽象出来的一个对象,使用此对象,我们可以定义显示的内容,并且可以通过移动相机位置来显示不同的内容。
下面讲解一下 Three.js 中相机的通用属性和常用的相机对象。
相机通用属性和方法
我们常用的相机有正交相机(OrthographicCamera)和透视相机(PerspectiveCamera)两种,用于来捕获场景内显示的物体模型。它们有一些通用的属性和方法。
Object3D 的所有属性和方法
由于相机都继承自 THREE.Object3D 对象,所以像设置位置的 position 属性、rotation 旋转和 scale 缩放属性,可以直接对相机对象设置。我们甚至还可以使用 add()
方法,给相机对象添加子类,移动相机它的子类也会跟随着一块移动,我们可以使用这个特性制作一些比如 HUD 类型的显示界面。
target 焦点属性和 lookAt() 方法
这两个方法的效果一定,都是调整相机的朝向,可以设置一个 THREE.Vector3
(三维向量)点的位置:
1
2
camera.target = new THREE.Vector3(0, 0, 0);
camera.lookAt(new THREE.Vector3(0, 0, 0));
上面两个都是朝向了原点,我们也可以将相机的朝向改为模型网格的 position,如果物体的位置发生了变化,相机的焦点方向也会跟随变动:
1
2
3
4
var mesh = new THREE.Mesh(geometry, material);
camera.target = mesh.position;
//或者
camera.lookAt(mesh.position);
getWorldDirection()
getWorldDirection() 方法可以获取当前位置到 target 位置的世界中的方向。方向也可以使用 THREE.Vector3
对象表示,所以该方法返回一个三维向量。
OrthographicCamera 正交相机
使用正交相机 OrthographicCamera 渲染出来的场景,所有的物体和模型都按照它固有的尺寸和精度显示,一般使用在工业要求精度或者 2D 平面中,因为它能完整的显示物体应有的尺寸。
创建正交相机

上面的图片可以清晰的显示出正交相机显示的范围,它显示的内容是一个立方体结构,通过图片我们发现,只要确定 top、left、right、bottom、near 和 far 六个值,我们就能确定当前相机捕获场景的区域,在这个区域外面的内容不会被渲染,所以,我们创建相机的方法就是:
1
new THREE.OrthographicCamera( left, right, top, bottom, near, far );
下面我们创建了一个显示场景中相机位置前方长宽高都为4的盒子内的物体的正交相机:
1
2
var orthographicCamera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0, 4);
scene.add(orthographicCamera); //一般不需要将相机放置到场景当中,如果需要添加子元素等一些特殊操作,还是需要add到场景内
正常情况相机显示的内容需要和窗口显示的内容为同样的比例才能够显示没有被拉伸变形的效果:
1
2
3
var frustumSize = 1000; //设置显示相机前方1000高的内容
var aspect = window.innerWidth / window.innerHeight; //计算场景的宽高比
var orthographicCamera = new THREE.OrthographicCamera( frustumSize * aspect / - 2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / - 2, 1, 2000 ); //根据比例计算出left,top,right,bottom的值
动态修改正交相机属性
我们也可以动态的修改正交相机的一些属性,注意修改完以后需要调用相机 updateProjectionMatrix() 方法来更新相机显存里面的内容,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
var frustumSize = 1000; //设置显示相机前方1000高的内容
var aspect = window.innerWidth / window.innerHeight; //计算场景的宽高比
var orthographicCamera = new THREE.OrthographicCamera(); //实例化一个空的正交相机
orthographicCamera.left = frustumSize * aspect / - 2; //设置left的值
orthographicCamera.right = frustumSize * aspect / 2; //设置right的值
orthographicCamera.top = frustumSize / 2; //设置top的值
orthographicCamera.bottom = frustumSize / - 2; //设置bottom的值
orthographicCamera.near = 1; //设置near的值
orthographicCamera.far = 2000; //设置far的值
//注意,最后一定要调用updateProjectionMatrix()方法更新
orthographicCamera.updateProjectionMatrix();
窗口变动后的更新
由于浏览器的窗口可以随意修改,我们有时候需要监听浏览器窗口的变化,然后获取到最新的宽高比,再重新设置相关属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
var aspect = window.innerWidth / window.innerHeight; //重新获取场景的宽高比
//重新设置left right top bottom 四个值
orthographicCamera.left = frustumSize * aspect / - 2; //设置left的值
orthographicCamera.right = frustumSize * aspect / 2; //设置right的值
orthographicCamera.top = frustumSize / 2; //设置top的值
orthographicCamera.bottom = frustumSize / - 2; //设置bottom的值
//最后,记得一定要更新数据
orthographicCamera.updateProjectionMatrix();
//显示区域尺寸变了,我们也需要修改渲染器的比例
renderer.setSize(window.innerWidth, window.innerHeight);
PerspectiveCamera 透视相机
透视相机是最常用的也是模拟人眼视角的一种相机,它所渲染生成的页面是一种近大远小的效果。
创建透视相机

上面的图片就是一个透视相机的生成原理,我们先看看渲染的范围是如何生成的:
- 首先,我们需要确定一个 fov 值,这个值是用来确定相机前方的垂直视角,角度越大,我们能够查看的内容就越多。
- 然后,我们又确定了一个渲染的宽高比,这个宽高比最好设置成页面显示区域的宽高比,这样我们查看生成画面才不会出现拉伸变形的效果,这时,我们可以确定前面生成内容的范围是一个四棱锥的区域。
- 最后,我们需要确定的就是相机渲染范围的最小值 near 和最大值 far,注意,这两个值都是距离相机的距离,确定完数值后,相机会显示的范围就是一个近小远大的四棱柱的范围,我们能够看到的内容都是在这个范围内的。
通过上面的原理,我们需要设置 fov 垂直角度,aspect 视角宽高比例和 near 最近渲染距离 far 最远渲染距离,就能确定当前透视相机的渲染范围。
下面代码展示了一个透视相机的创建方法:
1
2
var perspectiveCamera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
scene.add( perspectiveCamera );
我们设置了前方的视角为45度,宽度和高度设置成显示窗口的宽度除以高度的比例即可,显示距离为1到1000距离以内的物体。
动态修改透视相机的属性
透视相机的属性创建完成后我们也可以根据个人需求随意修改,但是注意,相机的属性修改完成后,以后要调用 updateProjectionMatrix()
方法来更新:
1
2
3
4
5
6
7
8
9
10
11
var perspectiveCamera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
scene.add( perspectiveCamera );
//下面为修改当前相机属性
perspectiveCamera.fov = 75; //修改相机的fov
perspectiveCamera.aspect = window.innerWidth/window.innerHeight; //修改相机的宽高比
perspectiveCamera.near = 100; //修改near
perspectiveCamera.far = 500; //修改far
//最后更新
perspectiveCamera.updateProjectionMatrix();
显示窗口变动后的回调
如果当前场景浏览器的显示窗口变动了,比如修改了浏览器的宽高后,我们需要设置场景自动更新,下面是一个常用的案例:
1
2
3
4
5
6
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight; //重新设置宽高比
camera.updateProjectionMatrix(); //更新相机
renderer.setSize(window.innerWidth, window.innerHeight); //更新渲染页面大小
}
window.onresize = onWindowResize;
最后,我写了一个案例来查看透视相机和正交相机的显示区别:点击这里案例 Demo。
左侧是透视相机显示的效果,这种更符合人眼看到的效果,场景更加的立体。而右侧则是正交相机实现的效果,渲染出来的数值更加准确,但是却不符合人眼查看的效果。
案例代码查看地址:点击这里。
制作相机控制器
在实际生产当中,很多时候我们需要切换相机的位置,以达到项目需求。 Three.js 也有很多相机控制插件,这个我们会在后面的课程当中讲解。
下面是我书写的一个小案例,能够通过鼠标左键拖拽界面让相机围绕模型周围查看模型。
首先,先上案例地址:点击这里。
代码查看地址:点击这里。
其实和以前的代码相比,我们也只是多出来了一个 initControl 方法,在这个方法里面绑定鼠标事件。实现的思路也很简单:
- 首先绑定鼠标按下事件,获取到按下时的相机位置和距离原点的距离,计算出来相对于 Z 轴正方向的偏移。绑定鼠标移动事件。
- 然后,在鼠标移动事件里面获取到距离鼠标按下的偏移,通过鼠标偏移数值计算出现在相机位置,并赋值。
思路就这么简单,虽然实现起来会麻烦,我也尽量写的简单,大家尽量能够自己也写出来。
粒子Points
我们将学习到 Sprite 精灵和 Points 粒子,这两种对象共同点就是我们通过相机查看它们时,始终看到的是它们的正面,它们总朝向相机。通过它们的这种特性,我们可以实现广告牌的效果,或实现更多的比如雨雪、烟雾等更加绚丽的特效。
Sprite 精灵
精灵由于一直正对着相机的特性,一般使用在模型的提示信息当中。通过 THREE.Sprite
创建生成,由于 THREE.Sprite
和 THREE.Mesh
都属于 THREE.Object3D
的子类,所以,我们操作模型网格的相关属性和方法大部分对于精灵都适用。和精灵一起使用的还有一个 THREE.SpriteMaterial
对象,它专门配合精灵的材质。注意,精灵没有阴影效果。
下面首先创建一个最简单的精灵:
1
2
3
var spriteMaterial = new THREE.SpriteMaterial( { color: 0xffffff } );
var sprite = new THREE.Sprite( spriteMaterial );
scene.add( sprite );
创建一个带有纹理图片的精灵:
1
2
3
4
var spriteMap = new THREE.TextureLoader().load( "sprite.png" );
var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff } );
var sprite = new THREE.Sprite( spriteMaterial );
scene.add( sprite );
直接使用 canvas 创建精灵:
1
2
3
4
var spriteMap = new THREE.Texture(canvas);
var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff } );
var sprite = new THREE.Sprite( spriteMaterial );
scene.add( sprite );
相关属性
精灵特有的属性就一个,即 center 属性,值是一个二维向量 THREE.Vector2
,这个属性意义就是当前设置的精灵位置点的位置处于精灵图中的位置。如果 center 的值是 0,0
旋转的位置就在左下角,如果值为 1,1
的话,那旋转的位置则在精灵图的右上角,默认值是 0.5,0.5
:
1
sprite.center.set(0.5, 0); //设置位置点处于精灵的最下方中间位置
接下来,我们查看一下精灵的案例:点击这里。
案例代码查看地址:点击这里。
案例效果从左到右依次是,普通的精灵,贴图纹理的精灵和 canvas
创建的精灵。
points 粒子
粒子和精灵的效果是一样的,它们之间的区别是,如果当前场景内的精灵过多的话,就会出现性能问题。粒子的作用就是为解决很多精灵而出现的,我们可以使用粒子去模型数量很多的效果,比如下雨,下雪等,数量很多的时候就适合使用粒子来创建,相应的,提高性能的损失就是失去了对单个精灵的操作,所有的粒子的效果都是一样。总的来说,粒子就是提高性能减少的一些自由度,而精灵就是为了自由度而损失了一些性能。
粒子的创建
粒子 THREE.Points
和精灵 THREE.Sprite
还有网格 THREE.Mesh
都属于 THREE.Object3D
的一个扩展,但是粒子有一些特殊的情况就是 THREE.Points
是它们粒子个体的父元素,它的位置设置也是基于 THREE.Points
位置而定位,而修改 THREE.Points
的 scale 属性只会修改掉粒子个体的位置。下面我们看下一个粒子的创建方法。创建一个粒子,需要一个含有顶点的几何体,和粒子纹理 THREE.PointsMaterial
的创建:
1
2
3
4
5
//球体
var sphereGeometry = new THREE.SphereGeometry(5, 24, 16);
var sphereMaterial = new THREE.PointsMaterial({color: 0xff00ff});
var sphere = new THREE.Points(sphereGeometry, sphereMaterial);
scene.add(sphere);
上面是通过球体几何体创建的一个最简单的粒子特效。
使用任何几何体都可以,甚至自己生成的几何体都可以,比如创建星空的案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
var starsGeometry = new THREE.Geometry();
//生成一万个点的位置
for (var i = 0; i < 10000; i++) {
var star = new THREE.Vector3();
//THREE.Math.randFloatSpread 在区间内随机浮动* - 范围 / 2 *到* 范围 / 2 *内随机取值。
star.x = THREE.Math.randFloatSpread(2000);
star.y = THREE.Math.randFloatSpread(2000);
star.z = THREE.Math.randFloatSpread(2000);
starsGeometry.vertices.push(star);
}
var starsMaterial = new THREE.PointsMaterial({color: 0x888888});
var starField = new THREE.Points(starsGeometry, starsMaterial);
scene.add(starField);
使用一个空的几何体,将自己创建的顶点坐标放入,也可以实现一组粒子的创建。如果我们需要单独设置每一个粒子的颜色,可以给 geometry 的 colors 数组添加相应数量的颜色:
1
2
3
4
5
6
7
8
9
10
for (var i = 0; i < 10000; i++) {
var star = new THREE.Vector3();
//THREE.Math.randFloatSpread 在区间内随机浮动* - 范围 / 2 *到* 范围 / 2 *内随机取值。
star.x = THREE.Math.randFloatSpread(2000);
star.y = THREE.Math.randFloatSpread(2000);
star.z = THREE.Math.randFloatSpread(2000);
starsGeometry.vertices.push(star);
starsGeometry.colors.push(new THREE.Color("rgb("+Math.random()*255+", "+Math.random()*255+", "+Math.random()*255+")")); //添加一个随机颜色
}
THREE.PointsMaterial 粒子的纹理
如果我们需要设置粒子的样式,还是需要通过设置 THREE.PointsMaterial
属性实现:
1
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff}); //设置了粒子纹理的颜色
我们还可以通过 PointsMaterial 的 size 属性设置粒子的大小:
1
2
3
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, size:4}); //粒子的尺寸改为原来的四倍
//或者直接设置属性
pointsMaterial.size = 4;
我们也可以给粒子设置纹理:
1
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, map:texture}); //添加纹理
默认粒子是不受光照影响的,我们可以设置 lights 属性为 true,让粒子受光照影响:
1
2
3
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, lights:true});
//或者
pointsMaterial.lights = true; //开启受光照影响
我们也可以设置粒子不受到距离的影响产生近大远小的效果:
1
2
3
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, sizeAttenuation: false});
//或者
pointsMaterial.sizeAttenuation = false; //关闭粒子的显示效果受距离影响
粒子的效果就介绍到这里,希望大家熟练了以后能够做出来各种各样的花哨效果。
接下来展示下我给大家准备的粒子案例:点击这里。
代码查看地址:点击这里。
这个案例和精灵的案例区别就是,将球体改成了粒子,然后将立方体修改成了带有 canvas 纹理的粒子,并且在背景里面添加了一万个粒子
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+Threejs学习和总结基础篇 - AIGISSS Threejs学习和总结基础篇
本文最后更新于:1 年前
概述
使用 Three.js 显示创建的内容,我们必须需要的三大件是:渲染器、相机和场景。相机获取到场景内显示的内容,然后再通过渲染器渲染到画布上面。
要在屏幕上展示3D图形,思路大体上都是这样的:
- 构建一个三维空间
- Three中称之为场景(Scene)
- 选择一个观察点,并确定观察方向/角度等
- Three中称之为相机(Camera)
- 在场景中添加供观察的物体
- Three中的物体有很多种,包括Mesh,Line,Points等,它们都继承自Object3D类
- 将观察到的场景渲染到屏幕上的指定区域
- Three中使用Renderer完成这一工作
拿电影来类比的话,场景对应于整个布景空间,相机是拍摄镜头,渲染器用来把拍摄好的场景转换成胶卷。场景 Scene
场景是所有物体的容器,也对应着我们创建的三维世界。场景允许你设置哪些对象被Three.js渲染以及渲染在哪里。在场景中放置对象、灯光和相机。
相机 Camera
1
2
3
4
5
// 初始化相机
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); // 实例化相机
camera.position.set(0, 0, 15);
}
相机
Three中的相机有两种,分别是正投影相机THREE.OrthographicCamera和透视投影相机THREE.PerspectiveCamera。

正交投影与透视投影的区别如上图所示,左图是正交投影,物体发出的光平行地投射到屏幕上,远近的方块都是一样大的;右图是透视投影,近大远小,符合我们平时看东西的感觉。
维基百科:三维投影
正交投影相机

可以近似地认为,视景体里的物体平行投影到近平面上,然后近平面上的图像被渲染到屏幕上。
透视投影相机

坐标系
Camera是三维世界中的观察者,为了观察这个世界,首先我们要描述空间中的位置,Three中使用采用常见的右手坐标系定位:

我们这里使用到的是 THREE.PerspectiveCamera
,这个相机模拟人眼看到的效果,就是具有透视的效果,近大远小。
第一行,我们实例化了一个透视相机,需要四个值,分别是视野、宽高比、近裁面和远裁面。
- 视野:当前相机视野的宽度,值越大,渲染出来的内容也会更多。
- 宽高比:默认是按照画布显示的宽高比例来设置,如果比例设置的不对,会发现渲染出来的画面有拉伸或者压缩的感觉。
- 近裁面和远裁面:这个是设置相机可以看到的场景内容的范围,如果场景内的内容位置不在这两个值内的话,将不会被显示到渲染的画面中。
第二行,我们设置了相机的位置。
渲染器 Renderer
1
2
3
4
5
6
//初始化渲染器
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高
document.body.appendChild(renderer.domElement); //添加到dom
}
第一行实例化了一个 THREE.WebGLRenderer
,这是一个基于 WebGL 渲染的渲染器,当然,Three.js 向下兼容,还有 CanvasRenderer、CSS2DRenderer、CSS3DRenderer 和 SVGRenderer,这四个渲染器分别基于 canvas2D、CSS2D、CSS3D 和 SVG 渲染的渲染器。由于,作为 3D 渲染,WebGL 渲染的效果最好,并且支持的功能更多。
第二行,调用了一个设置函数 setSize 方法,这个是设置需要显示的窗口大小。案例是基于浏览器全屏显示,所以设置了浏览器窗口的宽和高。
第三行,renderer.domElement
是在实例化渲染器时生成的一个 Canvas 画布,渲染器渲染界面生成的内容,都将在这个画布上显示。所以,我们将这个画布添加到了 DOM 当中,来显示渲染的内容。
模型 Object3D
渲染器,场景和相机都全了,是不是就能显示东西了?不能!因为场景内没有内容,即使渲染出来也是一片漆黑,所以我们需要往场景里面添加内容。接下来,我们将查看 initMesh 方法,看看如何创建一个最简单的模型:
1
2
3
4
5
6
7
8
//创建模型
function initMesh() {
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
}
创建一个网格(模型)需要两种对象:几何体和材质。
- 几何体代表模型的形状,它是由固定的点的位置组成,点绘制出面,面组成了模型。
- 材质是我们看到当前模型显示出来的效果,如显示的颜色,质感等。
Three中供显示的物体有很多,它们都继承自Object3D类
Mesh
我们都知道,计算机的世界里,一条弧线是由有限个点构成的有限条线段连接得到的。线段很多时,看起来就是一条平滑的弧线了。
计算机中的三维模型也是类似的,普遍的做法是用三角形组成的网格来描述,我们把这种模型称之为Mesh模型。

这是那只著名的斯坦福兔子。它在3D图形中的地位与数字图像处理领域中著名的Lenna是类似的。
看这只兔子,随着三角形数量的增加,它的表面越来越平滑
在Three中,Mesh的构造函数是这样的:Mesh( geometry, material )
geometry是它的形状,material是它的材质。
不止是Mesh,创建很多物体都要用到这两个属性。下面我们来看看这两个重要的属性。Material和Geometry是相辅相成的,必须结合使用。
Geometry
Geometry,形状,相当直观。Geometry通过存储模型用到的点集和点间关系(哪些点构成一个三角形)来达到描述物体形状的目的。
Three提供了立方体(其实是长方体)、平面(其实是长方形)、球体、圆形、圆柱、圆台等许多基本形状;
你也可以通过自己定义每个点的位置来构造形状;
对于比较复杂的形状,我们还可以通过外部的模型文件导入。
Material
Material,材质,这就没有形状那么直观了。
材质其实是物体表面除了形状以为所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度。
这里讲一下材质(Material)、贴图(Map)和纹理(Texture)的关系。
材质上面已经提到了,它包括了贴图以及其它。
贴图其实是‘贴’和‘图’,它包括了图片和图片应当贴到什么位置。
纹理嘛,其实就是‘图’了。
Three提供了多种材质可供选择,能够自由地选择漫反射/镜面反射等材质。
光影Light
神说:要有光!
光影效果是让画面丰富的重要因素。
Three提供了包括环境光AmbientLight、点光源PointLight、 聚光灯SpotLight、方向光DirectionalLight、半球光HemisphereLight等多种光源。
只要在场景中添加需要的光源就好了。
让场景动起来
动画,就是多幅图片一直切换便可显示动画的效果。为了能显示动画的效果,我们首先要了解一个函数 requestAnimationFrame,这个函数专门为了动画而出现。它与 setInterval 相比,优势在于不需要设置多长时间重新渲染,而是在当前线程内 JS 空闲时自动渲染,并且最大帧数控制在一秒60帧。所以,我们书写了一个可以循环调用的函数:
1
2
3
4
function animate() {
requestAnimationFrame(animate); //循环调用函数
// ...
}
在循环调用的函数中,每一帧我们都让页面重新渲染相机拍摄下来的内容:
1
renderer.render( scene, camera ); //渲染界面
渲染的 render 方法需要两个值,第一个值是场景对象,第二个值是相机对象。这意味着,你可以有多个相机和多个场景,可以通过渲染不同的场景和相机让画布上显示不同的画面。
但是,如果现在一直渲染的话,我们发现就一个立方体在那,也没有动,我们需要做的是让立方体动起来:
1
2
mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度
mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度
每一个实例化的网格对象都有一个 rotation 的值,通过设置这个值可以让立方体旋转起来。在每一帧里,我们让立方体沿 x 轴方向旋转0.01弧度,沿 y 轴旋转0.02弧度(1π 弧度等于180度角度)。
Threejs性能检测插件
在 Three.js 里面,遇到最多的问题就是性能问题,所以我们需要时刻检测当前的 Three.js 的性能。现在 Three.js 常使用的一款插件叫 stats。接下来我们看看如何将 stats 插件在 Three.js 的项目中使用。
- 首先在页面中引入插件代码:
1
<script src="http://www.wjceo.com/lib/js/libs/stats.min.js"></script>
这是 一个 CDN 的地址,直接引入即可。
- 然后,我们需要实例化一个 stats 对象,然后把对象内生成的 DOM 添加到页面当中。
1
2
stats = new Stats();
document.body.appendChild(stats.dom);
- 最后一步,我们需要在 requestAnimationFrame 的回调里面更新每次渲染的时间:
1
2
3
4
5
function animate() {
requestAnimationFrame(animate); //循环调用函数
stats.update(); //更新性能插件
renderer.render( scene, camera ); //渲染界面
}
使用了性能检测插件以后,整个代码如下:
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Stats插件案例</title>
<style>
body {
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
</head>
<body onload="init()">
<script src="https://cdn.bootcss.com/three.js/92/three.js"></script>
<script src="http://www.wjceo.com/lib/js/libs/stats.min.js"></script>
<script>
//声明一些全局变量
var renderer, camera, scene, geometry, material, mesh, stats;
//初始化渲染器
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高
document.body.appendChild(renderer.domElement); //添加到dom
}
//初始化场景
function initScene() {
scene = new THREE.Scene(); //实例化场景
}
//初始化相机
function initCamera() {
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
); //实例化相机
camera.position.set(0, 0, 15);
}
//创建模型
function initMesh() {
geometry = new THREE.BoxGeometry(2, 2, 2); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh(geometry, material); //创建网格
scene.add(mesh); //将网格添加到场景
}
//运行动画
function animate() {
requestAnimationFrame(animate); //循环调用函数
mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度
mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度
stats.update(); //更新性能检测框
renderer.render(scene, camera); //渲染界面
}
//性能检测框
function initStats() {
stats = new Stats();
document.body.appendChild(stats.dom);
}
//初始化函数,页面加载完成是调用
function init() {
initRenderer();
initScene();
initCamera();
initMesh();
initStats();
animate();
}
</script>
</body>
</html>
场景Scene
场景是我们每个 Three.js 项目里面放置内容的容器,我们也可以拥有多个场景进行切换展示,你可以在场景内放置你的模型、灯光和照相机。还可以通过调整场景的位置,让场景内的所有内容都一起跟着调整位置。

场景的结构
之前在刚刚开始学 JavaScript 基础时,我们总免不了去操作 DOM 对象,而且我们都知道 DOM 的结构是树形结构的,Three.js 也遵循了这样的理念,将所有可以添加到场景内的结构梳理成了一种树形的结构,方便我们能够更好的理解Three.js。
我们可以把 Scene 想象成一个 body,body 内可以添加 DOM 对象,scene 内也可以添加它的 3D 对象,这样一层层的嵌套出来,组成了我们现在需要的项目。所以,在 Three.js 中,为了方便操作,将所有 3D 对象共同的内容抽象成了一个基类,就是 THREE.Object3D
。
THREE.Object3D
为了方便操作,Three.js 将每个能够直接添加到场景内的对象都继承自一个基类——THREE.Object3D
,以后我们将继承自这个基类的对象称为 3D 对象,判断一个对象是否是继承自 THREE.Object3D
,我们可以这么做:
1
2
obj instanceof THREE.Object3D
//继承至返回 true 否则返回false
这个基类上封装了我们常用的一些方法,下面我们分别介绍下。
添加一个 3D 对象
1
scene.add(mesh); //将网格添加到场景
将一个立方体添加到场景内显示。
这个方法不光能够在场景内使用,而且也可以将一个 3D 对象添加到另一个 3D 对象里面,代码如下:
1
parent.add(child);
获取一个 3D 对象
获取一个 3D 对象可以使用 getObjectByName 通过 3D 对象的 name 值进行获取,在获取前我们首先要设置当前 3D 对象的 name 值:
1
2
3
object3D.name = "firstObj";
scene.add(object3D);
scene.getObjectByName("firstObj"); //返回第一个匹配的3d对象
另一种方式就是使用 getObjectById 通过 3D 对象的 id 值进行获取,3D 对象的 id 值只能读取,它是在添加到场景时,按 1、2、3、4、5……的顺序默认生成的一个值,无法自定义:
1
scene.getObjectById(1); //返回id值为1的3d对象
删除一个 3D 对象
如果我们想隐藏一个 3D 对象,而不让它显示,可以通过设置它的 visible的值来实现:
1
mesh.visible = false; //设置为false,模型将不会被渲染到场景内
如果一个模型不再被使用到,需要彻底删除,我们可以使用 remove 方法进行删除:
1
2
scene.add(mesh); //将一个模型添加到场景当中
scene.remove(mesh); //将一个模型从场景中删除
获取到所有的子类
每一个 3D 对象都有一个 children 属性,这是一个数组,里面包含所有添加的 3D 对象:
1
2
3
4
scene.add(mesh1);
scene.add(mesh2);
console.log(scene.children);
// [mesh1, mesh2]
如果想获取 3D 对象下面所有的 3D 对象,我们可以通过 traverse方法获取:
1
2
3
4
5
6
mesh1.add(mesh2); //mesh2是mesh1的子元素
scene.add(mesh1); //mesh1是场景对象的子元素
scene.traverse(fucntion(child){
console.log(child);
});
//将按顺序分别将mesh1和mesh2打印出来
获取 3D 对象的父元素
每个 3D 对象都有一个父元素,可以通过 parent 属性进行获取:
1
2
scene.add(mesh); //将模型添加到场景
console.log(mesh.parent === scene); //true
修改 3D 对象
前面介绍了场景的结构以及场景的 3D 对象添删查,下面,我们接着介绍对场景内模型的一些操作。
修改位置方式
我们可以通过设置模型的 position 属性来修改模型的当前位置,具体方法有以下几种。
- 单独设置每个方向的属性。
1
2
3
mesh.position.x = 3; //将模型的位置调整到x正轴距离原点为3的位置。
mesh.position.y += 5; //将模型的y轴位置以当前的位置向上移动5个单位。
mesh.position.z -= 6;
- 直接一次性设置所有方向的属性。
1
mesh.position.set(3, 5, -6); //直接将模型的位置设置在x轴为3,y轴为5,z轴为-6的位置
- Three.js 模型的位置属性是一个
THREE.Vector3
(三维向量)的对象,我们可以直接重新赋值一个新的对象。
1
mesh.position = new THREE.Vector3(3, 5, -6); //上面的设置位置也可以通过这样设置。
修改大小的方式
模型导入后,很多情况下都需要调整模型的大小。我们可以通过设置模型的 scale 属性来调整大小。
- 第一种方式是单独设置每个方向的缩放。
1
2
3
mesh.scale.x = 2; //模型沿x轴放大一倍
mesh.scale.y = 0.5; //模型沿y轴缩小一倍
mesh.scale.z = 1; //模型沿z轴保持不变
- 第二种是使用 set 方法。
1
2
mesh.scale.set(2, 2, 2); //每个方向等比放大一倍
mesh.scale.set(0.5, 0.5, 0.5); //每个方向等比缩小一倍
- 第三种方式,由于 scale 属性也是一个三维向量,我们可以通过赋值的方式重新修改。
1
mesh.scale = new THREE.Vector3(2, 2, 2); //每个方向都放大一倍
修改模型的转向
很多情况下,我们需要对模型进行旋转,以达到将模型显示出它需要显示的方位,我们可以通过设置模型的 rotation 属性进行旋转(注意:旋转 Three.js 使用的是弧度不是角度)。
- 第一种方式是单独设置每个轴的旋转。
1
2
3
mesh.rotation.x = Math.PI; //模型沿x旋转180度
mesh.rotation.y = Math.PI * 2; //模型沿y轴旋转360度,跟没旋转一样的效果。。。
mesh.rotation.z = - Math.PI / 2; //模型沿z轴逆时针旋转90度
- 第二种方式就是使用 set 方法重新赋值。
1
mesh.rotation.set(Math.PI, 0, - Math.PI / 2); //旋转效果和第一种显示的效果相同
正常模型的旋转方式是按照 XYZ 依次旋转的,如果你想先旋转其他轴,我们可以添加第四项修改,有可能的情况为:YZX、ZXY、XZY、YXZ 和 ZYX。
1
mesh.rotation.set(Math.PI, 0, - Math.PI / 2, "YZX"); //先沿y轴旋转180度,再沿z轴旋转0度,最后沿x轴逆时针旋转90度
- 第三种方式,模型的 rotation 属性其实是一个欧拉角对象(
THREE.Euler
),欧拉角后面会讲解到,我们可以通过重新赋值一个欧拉角对象来实现旋转调整:
1
mesh.rotation = new THREE.Euler(Math.PI, 0, - Math.PI / 2, "YZX");
使用 dat.GUI 实现页面调试
有些时候,我们需要调整模型的位置或者大小等,且需要每次都去场景内调试,一种常用的插件 dat.GUI
- 首先,需要将插件的源码引入到页面当中,我这里直接使用 CDN 的连接。
1
<script src="https://cdn.bootcss.com/dat-gui/0.7.1/dat.gui.min.js"></script>
- 创建一个对象,在里面设置我们需要修改的一些数据。
1
2
3
4
5
controls = {
positionX:0,
positionY:0,
positionZ:0
};
- 实例化
dat.GUI
对象,将需要修改的配置添加对象中,并监听变化回调。
1
2
3
4
5
6
7
8
gui = new dat.GUI();
gui.add(controls, "positionX", -1, 1).onChange(updatePosition);
gui.add(controls, "positionY", -1, 1).onChange(updatePosition);
gui.add(controls, "positionZ", -1, 1).onChange(updatePosition);
function updatePosition() {
mesh.position.set(controls.positionX, controls.positionY, controls.positionZ);
}

这样,只要我们每次修改对象里面的值,都会触发 updatePosition
回调,来更新模型的位置。这样,我们就实现了一个简单的案例。
接下来,我列出一下经常会使用到一些方式和方法。
生成一个输入框
dat.GUI
能够根据 controls 值的不同而生成不同的操作方法,如果值的类型为字符串或者数字类型,则可以生成一个默认的输入框:
1
2
3
gui.add(controls, "positionX");
gui.add(controls, "positionY");
gui.add(controls, "positionZ");

生成一个可以滑动的滑块
使用 gui.add()
方法,如果值为数字类型,传入的第三个值(最小值)和第四个值(最大值),就限制了值能够取值的范围,这样就生成了可以滑动的滑块:
1
2
3
gui.add(controls, "positionX", -1, 1); //设置了最小值和最大值,可以生成滑块
gui.add(controls, "positionY").max(1); //只设置了最大值,无法生成滑块
gui.add(controls, "positionZ").min(-1); //只设置了最小值,也无法生成滑块

我们还可以通过 step() 方法来限制每次变化的最小值,也就是你增加或者减少,必须都是这个值的倍数:
1
2
3
gui.add(controls, "positionX", -10, 10).step(1); //限制必须为整数
gui.add(controls, "positionY", -10, 10).step(0.1); //每次变化都是0.1的倍数
gui.add(controls, "positionZ", -10, 10).step(10); //每次变化都是10的倍数

生成一个下拉框
只要按规则在 gui.add()
的第三个值传入一个对象或者数组,dat.GUI
就能够自动匹配生成一个下拉框:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:0,
positionY:false,
positionZ:"middle"
};
gui = new dat.GUI();
gui.add(controls, "positionX", {left:-10, middle:0, right:10}); //数字类型的下拉框
gui.add(controls, "positionY", [true, false]); //布尔值类型的下拉框
gui.add(controls, "positionZ", ["left", "middle", "right"]); //字符串类型的下拉框

生成一个 Checkbox
只要 controls 的值是一个布尔类型,使用 gui.add()
方法就可以生成一个复选框:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:true,
positionY:false,
positionZ:false
};
gui = new dat.GUI();
gui.add(controls, "positionX");
gui.add(controls, "positionY");
gui.add(controls, "positionZ");

生成一个点击事件按钮
如果 controls 的值为一个函数 Function,dat.GUI
会自动生成一个可以点击的按钮,当按下时就会触发这个函数事件:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:function () {},
positionY:function () {},
positionZ:function () {}
};
gui = new dat.GUI();
gui.add(controls, "positionX");
gui.add(controls, "positionY");
gui.add(controls, "positionZ");

修改显示的名称
我们可以在后面使用 name() 事件设置显示的名称:
1
2
3
gui.add(controls, "positionX", -1, 1).name("x轴");
gui.add(controls, "positionY", -1, 1).name("y轴");
gui.add(controls, "positionZ", -1, 1).name("z轴");

颜色选择框
实现颜色选择框,首先需要一种正常格式的颜色值,比如 CSS 的颜色样式或者 RGB 格式,然后再使用 gui.addColor()
的方法添加:
1
2
3
4
5
6
7
8
9
10
controls = {
positionX:"#cccccc", //css样式
positionY: [0, 255, 255], //RGB格式
positionZ: [0, 255, 255, 0.6] //RGBA格式
};
gui = new dat.GUI();
gui.addColor(controls, "positionX").name("x轴");
gui.addColor(controls, "positionY").name("y轴");
gui.addColor(controls, "positionZ").name("z轴");

监听事件回调
dat.GUI
给我们提供了监听事件回调的方法 onChange(),如果值变化就能够触发函数回调:
1
2
3
4
5
6
7
gui.add(controls, "positionX", -1, 1).onChange(updatePosition);
gui.add(controls, "positionY", -1, 1).onChange(updatePosition);
gui.add(controls, "positionZ", -1, 1).onChange(updatePosition);
function updatePosition() {
mesh.position.set(controls.positionX, controls.positionY, controls.positionZ);
}
创建分组
我们可以使用 gui.addFolder()
方法来创建分组:
1
2
3
4
5
6
7
8
gui = new dat.GUI();
var first = gui.addFolder("第一个分组"); //创建第一个分组
first.add(controls, "positionX", -1, 1).onChange(updatePosition);
first.open();
var two = gui.addFolder("第二个分组");
two.add(controls, "positionY", -1, 1).onChange(updatePosition);
two.add(controls, "positionZ", -1, 1).onChange(updatePosition);

几何体Geometry
一个模型是由几何体 Geometry 和材质 Material 组成。Three.js 内置了很多的几何体种类,如立方体、三棱锥、球、八面体、十二面体、二十面体等等,将介绍这些几何体的模型创建和几何体的通用方法。

Geometry 和 BufferGeometry
当前 Three.js 内置了这两种几何体类型,这两个几何体类型都用于存储模型的顶点位置、面的索引、法向量、颜色、UV 纹理以及一些自定义的属性。
它们两个的区别是:BufferGeometry 存储的都是一些原始的数据,性能比 Geometry 高,很适合存储一些放入场景内不需要再额外操作的模型。而 Geometry 的优势刚好相反,Geometry 比 BufferGeometry 更友好,使用了 Three.js 提供的 THREE.Vector3
或者 THREE.Color
这样的对象来存储数据(顶点位置、面、颜色等),这些对象易于阅读和编辑,但效率低于 BufferGeometry 使用的类型化数组。
所以,我们可以根据项目的大小来使用不同的几何体,小项目可以使用 Geometry 实现,中大型的项目还是推荐 BufferGeometry。
我们将使用较为简单的 Geometry 来实现案例。
Geometry 和 BufferGeometry 互转
在这里插一点内容,两种几何体类型可以互转,所以,不要担心现在使用的是哪种。
- Geometry 转换成 BufferGeometry
转换代码,如下:
1
2
3
4
5
//实例化一个Geometry对象
var geo = new THREE.Geometry();
//调用对象的fromBufferGeometry方法,并将需要转换的geometry传入
var bufferGeo = geo.fromBufferGeometry(geometry);
//返回的对象转换成的BufferGeometry
BufferGeometry
转换成Geometry
1
2
3
4
5
//实例化一个BufferGeometry对象
var bufferGeo = new THREE.BufferGeometry();
//调用对象的fromGeometry方法,并将需要转换的bufferGeometry传入
var geo = bufferGeo.fromGeometry(bufferGeometry);
//返回的对象转换成的Geometry
接下来,我们将讲解一下 Three.js 内置几何体。
立方体 BoxGeometry 和 BoxBufferGeometry
立方体是最早接触的几何体,可以通过设置长宽高来创建各种各样的立方体。
看下面的案例,代码:
1
2
3
4
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );
案例中的构造函数:
1
BoxGeometry(width : '浮点类型', height : '浮点类型', depth : '浮点类型', widthSegments : '整数类型', heightSegments : '整数类型', depthSegments : '整数类型')
各参数的含义:
- width:沿 X 轴的宽度,默认值为1;
- height:沿 Y 轴的高度,默认值为1;
- depth:沿 Z 轴的深度,默认值为1;
- widthSegments:可选,沿着边的宽度的分割面的数量。默认值为1;
- heightSegments:可选,沿着边的高度的分割面的数量。默认值为1;
- depthSegments:可选,沿着边的深度的分割面的数量。缺省值是1;
案例查看:请点击这里。
圆 CircleGeometry 和 CircleBufferGeometry
在 WebGL 里,所有的模型都是通过三角形面组成,圆形是由多个三角形分段构成,这些三角形分段围绕一个中心点延伸并且延伸到给定半径以外。它从起始角度和给定的中心角度逆时针方向构建。它也可以用来创建规则的多边形,其中线段的数量决定了边的数量。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.CircleGeometry( 5, 32 );
var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
var circle = new THREE.Mesh( geometry, material );
scene.add( circle );
案例中的构造函数,如下:
1
CircleGeometry(radius : '浮点类型', segments : '整数类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数的含义:
- radius:圆的半径,默认值为1;
- segments:段数(三角形),最小值为3,默认值为8;
- thetaStart:第一段的起始角度,默认值为0;
- thetaLength:圆形扇形的中心角,通常称为 theta。默认值是 2 * Pi,画出一个整圆。
案例查看:请点击这里。
圆锥 ConeGeometry 和 ConeBufferGeometry
这是一个可以创建圆锥体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.ConeGeometry( 5, 20, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var cone = new THREE.Mesh( geometry, material );
scene.add( cone );
案例中的构造函数,如下:
1
ConeGeometry(radius : '浮点类型', height : '浮点类型', radialSegments : '整数类型', heightSegments : '整数类型', openEnded : '布尔类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数含义:
- radius:底部圆锥的半径,默认值为1;
- height:圆锥体的高度,默认值为1;
- radialSegments:圆锥周围的分段面数,默认值为8;
- heightSegments:沿圆锥体高度的面的行数,默认值为1;
- openEnded:圆锥体底部是是隐藏还是显示,默认值为 false,显示;
- thetaStart:第一段的起始角度,默认值是0(Three.js 的0度位置)
- thetaLength — 圆形扇形的中心角,通常称为 theta。默认值是2 * Pi,画出一个整圆。
案例查看:请点击这里。
圆柱 CylinderGeometry 和 CylinderBufferGeometry
这是一个可以创建圆柱几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.CylinderGeometry( 5, 5, 20, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var cylinder = new THREE.Mesh( geometry, material );
scene.add( cylinder );
案例中的构造函数,如下:
1
CylinderGeometry(radiusTop : '浮点类型', radiusBottom : '浮点类型', height : '浮点类型', radialSegments : '整数类型', heightSegments : '整数类型', openEnded : '布尔类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数含义:
- radiusTop:顶部圆柱体的半径。默认值为1;
- radiusBottom:底部圆柱体的半径。默认值为1;
- height:圆柱体的高度。默认值为1;
- radialSegments:圆柱周围的分段面数。默认值为8;
- heightSegments:沿圆柱体高度的面的行数。默认值为1;
- openEnded:圆柱体的两端是否显示,默认值是 false,显示;
- thetaStart:第一段的起始角度,默认值是0(Three.js 的0度位置)。
- thetaLength — 圆形扇形的中心角,通常称为 theta。默认值是2 * Pi,画出一个整圆。
案例查看:请点击这里。
球 SphereGeometry 和 SphereBufferGeometry
这是一个可以创建球体几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.SphereGeometry( 5, 32, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var sphere = new THREE.Mesh( geometry, material );
scene.add( sphere );
案例中的构造函数,如下 :
1
SphereGeometry(radius : '浮点类型', widthSegments : '整数类型', heightSegments : '整数类型', phiStart : '浮点类型', phiLength : '浮点类型', thetaStart : '浮点类型', thetaLength : '浮点类型')
各参数含义:
- radius:球体半径。默认值是1;
- widthSegments:水平线段的数量。最小值是3,默认值是8;
- heightSegments:垂直段的数量。最小值是2,默认值是6;
- phiStart:指定水平渲染起始角度。默认值为0;
- phiLength:指定水平渲染角度大小。默认值是 Math.PI * 2;
- thetaStart:指定垂直渲染起始角度。默认值为0;
- thetaLength:指定垂直渲染角度大小。默认是 Math.PI。
默认是渲染整个的圆,如果线段越多,显得球体越圆滑。
案例查看:请点击这里。
平面 PlaneGeometry 和 SphereBufferGeometry
这是一个可以创建平面几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.PlaneGeometry( 5, 20, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
var plane = new THREE.Mesh( geometry, material );
scene.add( plane );
案例中的构造函数,如下 :
1
PlaneGeometry(width : '浮点类型', height : '浮点类型', widthSegments : '整数类型', heightSegments : '整数类型')
各参数含义:
- width:沿 X 轴的宽度。默认值为1;
- height:沿着 Y 轴的高度。默认值为1;
- widthSegments:宽度的分段数,可选。默认值为1;
- heightSegments:高度的分段数,可选。默认值为1。
案例查看:请点击这里。
圆环 TorusGeometry 和 TorusBufferGeometry
一个可以创建圆环几何体的类。
看下面案例,代码如下:
1
2
3
4
var geometry = new THREE.TorusGeometry( 10, 3, 16, 100 );
var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
var torus = new THREE.Mesh( geometry, material );
scene.add( torus );
案例中的构造函数,如下 :
1
TorusGeometry(radius : '浮点类型', tube : '浮点类型', radialSegments : '整数类型', tubularSegments : '整数类型', arc : '浮点类型')
各参数含义:
- radius:圆环的半径,从圆环的中心到管的中心。默认值为1;
- tube:管的半径。默认值是0.4;
- radialSegments:横向分段数,默认值是8;
- tubularSegments:纵向分段数,默认值是6;
- arc — 绘制的弧度。默认值是 Math.PI * 2,绘制整个圆环。
案例查看:请点击这里。
以上是 Three.js 内置的一些基础的几何体,Three.js 还内置了一些其他的几何体模型(如字体几何体、拉伸几何体、车床几何体等),由于篇幅原因和难度原因,在这里不一一讲解。
如果迫不及待得想了解这一块的内容可以通过官方文档或者我的博客中关于 Three.js 的笔记来了解更多。
常用方法
Geometry 和 BufferGeomety 内置了一些常用的方法,在每一种几何体上面,我们都可以调用相关的方法来达到我们的目的。
center()
此方法为居中方法,可以根据边界框居中几何图形。
computeBoundingBox()
此方法可以计算几何的边界框,方法调用后,会更新 Geometry.boundingBox
属性,我们可以通过 Geometry.boundingBox
属性获取到一个包围几何体的立方体的每个轴向的最大值和最小值。
dispose()
将几何体从内存中删除,这个方法必须记得使用。如果频繁的删除模型,一定要记得将几何体从内存中删除掉。
总结案例
最后,我将上面介绍的所有几何体放到一个场景内,制作了一个小案例,案例代码地址如下:
材质Material
模型的表现,也就是我们看到的的模型外观——材质。

简单的说,就是物体看起来是什么质地。材质可以看成是材料和质感的结合。在渲染程序中,它是表面各种可视属性的结合,这些可视属性是指表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等。Three.js 给我们封装好了大部分的材质效果,避免我们使用复杂的 Shader 语言自己去实现。接下来我们先介绍下 Material 常用的一些属性和方法。
基本属性和方法
needsUpdate
如果修改了 Material 内的内容,需要将 needsUpdate 属性设置为 true,Three.js 会在下一帧里将修改内容同步到 WebGL 的显存内。切记不要在 requestAnimationFrame 方法内更新,会浪费性能,只需要在更新 Material 属性后设置一次即可。
side
此属性可以定义当前面的哪个方向会被渲染,默认值是 THREE.FrontSide
(只渲染正面),可选值有:THREE.BackSide
(只渲染背面)和 THREE.DoubleSide
(正面和背面都会渲染)。
transparent
此属性定义了材质是否可以透明,因为对于透明需要材质进行特殊处理,并在不透明的物体渲染完成后再渲染透明物体。当设置此属性为true
后,可以通过设置opacity
来调整透明度,默认为false
。
opacity
此属性可以定义材质的透明度,必须将材质的 transparent 设置为 true 才可使透明度管用。取值范围为 0.0 到 1.0。默认值是1.0,也就是不透明。
map
此属性可以配置当前材质的纹理贴图,是一个 THREE.Texture
对象,下面我们将会讲解如何给材质贴图。这是大部分材质都会有的属性,只有极其个别的材质如 LineBasicMaterial
(线材质)等没有这个属性。
wireframe
是否将模型渲染成线框,默认为 false。个别材质也没有这个属性。
dispose()
此方法用于将材质从内存中删除,在不需要使用当前材质时使用,但不会将材质的纹理贴图删除,如果需要将纹理贴图也删除,需要调用 material.map.dispose()
。
配置纹理贴图
由于经常使用纹理贴图,所以在这里单独讲解一下如何实现一个纹理贴图。 实现纹理贴图有以下两种方式。
第一种,使用 THREE.TextureLoader
进行生成纹理对象:
1
2
var texture = new THREE.TextureLoader().load( "textures/water.jpg" );
material.map = texture; //将纹理赋值给材质
或者直接实例化:
1
2
var texture = new THREE.Texture(canvas); //实例化的第一个对象可以是`img`、`canvas`和`video`。
material.map = texture; //将纹理赋值给材质
纹理重复问题
如果图片不是标准的2的幂数(2、4、8、16、32、64、128、256、512、1024、2048……),在控制台会给我们提示:“THREE.WebGLRenderer: image is not power of two”,意思就是说图片不是标准格式高宽不是2的幂数。我们需要的水平方向和垂直方向上设置的图片重复显示。需要配置的两个属性是:texture.wrapS
(水平方向重复)和 texture.wrapT
(垂直方向重复),默认值是:THREE.ClampToEdgeWrapping
,即纹理的最后一个像素延伸到网格的边缘。可选项有:THREE.RepeatWrapping
,表示纹理将重复无穷大;MirroredRepeatWrapping
,表示镜像重复,可以理解为重复时,反着绘制一个然后正着绘制一个,达到的效果就是没有强烈的过渡感觉。
needsUpdate 属性
如果更新了纹理的相关属性,需要将此属性设置为 true,将数据同步到 WebGL。
repeat
纹理在整个表面水平方向和垂直方向重复多少次,也会受纹理重复设置的影响,设置方式为:
1
2
3
4
var texture = new THREE.TextureLoader().load( "textures/water.jpg" );
texture.wrapS = THREE.RepeatWrapping; //设置水平方向无限循环
texture.wrapT = THREE.RepeatWrapping; //设置垂直方向无限循环
texture.repeat.set( 4, 4 ); //水平方向和垂直方向都重复四次
内置常用材质
在讲解常用材质之前,我们先讲解一下如何实例化一个材质和一些需要注意的地方。我们使用第一个讲到的材质 MeshBasicMaterial 作为例子。
MeshBasicMaterial 和设置颜色的方法
这种材质是一种简单的材质,不会受到光的影响,直接看到的效果就是整个物体的颜色都是一样,没有立体的感觉。在实例化材质时,我们可以传入一个对象,设置材质的相关属性可以通过对象属性的方式传入,但是属性 color(颜色)例外,实例化的时候可以传入十六进制数,也可以写十六进制字符串。实例化完成后再修改需要重新赋值 THREE.Color
对象,或者调用 material.color.set
方法赋值。
1
2
3
4
5
var material = new THREE.MeshBasicMaterial({color:0x00ffff});
var geometry = new THREE.BoxGeometry(1, 1, 1);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
上面的案例就是使用 MeshBasicMaterial 材质创建了一个立方体,我们设置了显示颜色为一种浅蓝色,除了上面实例化的时候进行设置,后面也可以再修改:
1
2
var material = new THREE.MeshBasicMaterial({color:0x00ffff}); //设置初始的颜色为浅蓝色
material.color.set(0xff00ff); //将颜色修改为紫色
我们也可以直接赋值一个新的 THREE.Color
对象,如:
1
2
var material = new THREE.MeshBasicMaterial({color:0x00ffff}); //设置初始的颜色为浅蓝色
material.color = new THREE.Color(0xff00ff); //将颜色修改为紫色
我们可以通过 new THREE.Color
创建一个颜色对象,Three.js 支持的颜色书写方式比较丰富,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//直接传入十六进制数或者字符串
var color = new THREE.Color( 0xff0000 );
var color = new THREE.Color( "#ff0000" );
//RGB 字符串
var color = new THREE.Color("rgb(255, 0, 0)");
var color = new THREE.Color("rgb(100%, 0%, 0%)");
//支持一百四十多中颜色名称
var color = new THREE.Color( 'skyblue' );
//HSL 字符串
var color = new THREE.Color("hsl(0, 100%, 50%)");
//支持RGB值设置在0到1之间的方式
var color = new THREE.Color( 1, 0, 0 );
MeshNormalMaterial 法向材质
这种材质会根据面的方向不同自动改变颜色,也是我们之前一直在用的材质。此材质不受灯光影响。
1
2
3
4
5
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshNormalMaterial(); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
LineBasicMaterial 线条材质
在上一篇我们讲几何体时,没有讲解如何画直线,是由于直线需要单独的材质进行实现,所以我们将直线放到了材质这一篇中进行讲解。注意,由于 Windows 系统的原因,线的宽度只能为1。
要绘制线段,我们需要确定两个点,就是起点和终点,案例中我们使用了四个顶点创建了三条线。然后 Geometry 对象使用这组顶点配置几何体,实例化线的材质,最后使用 THREE.Line
生成线。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//添加直线
var pointsArr = [
new THREE.Vector3( -10, 0, -5 ),
new THREE.Vector3( -5, 15, 5 ),
new THREE.Vector3( 20, 15, -5 ),
new THREE.Vector3( 10, 0, 5 )
];
var lineGeometry = new THREE.Geometry(); //实例化几何体
lineGeometry.setFromPoints(pointsArr); //使用当前点的属性配置几何体
var lineMaterial = new THREE.LineBasicMaterial({color:0x00ff00}); //材质
line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line);
LineDashedMaterial 虚线
我们也可以创建虚线,这里我们来点新花样,就是实现曲线。曲线也和直线一样,在 Windows 系统线的粗度只能为1。
要创建曲线,我们需要使用到 THREE.CatmullRomCurve3
来生成一个 curve 对象,这是一个曲线对象,可以从对象获取生成的曲线的点的集合,在这里科普一下,曲线也是由无数段的直线组成的,段数分的越清晰,曲线过渡越顺滑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var pointsArr = [
new THREE.Vector3( -10, 0, -5 ),
new THREE.Vector3( -5, 15, 5 ),
new THREE.Vector3( 20, 15, -5 ),
new THREE.Vector3( 10, 0, 5 )
];
//指定一些用于生成曲线线的三维顶点
var curve = new THREE.CatmullRomCurve3(pointsArr);
var points = curve.getPoints( 50 ); //使用getPoints获取当前曲线分成50段后的所有顶点
var curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); //使用顶点生成几何体
var curveMaterial = new THREE.LineDashedMaterial( { color : 0xff0000 } ); //创建一条红色的线材质
// 使用THREE.Line创建线
curveLine = new THREE.Line( curveGeometry, curveMaterial );
curveLine.computeLineDistances(); //需要重新计算位置才能显示出虚线
scene.add(curveLine);
添加光
由于 MeshBasicMaterial 不会受光的影响,即使有光也不会影响它的效果,前面我们也没有添加光。但是后面介绍的材质会受到光源的影响,在介绍之前,我们需要添加一个光源,来影响材质的显示效果。
1
2
3
4
5
6
7
8
9
//创建灯光
function initLight() {
var light = new THREE.DirectionalLight(0xffffff); //添加了一个白色的平行光
light.position.set(20, 50, 50); //设置光的方向
scene.add(light); //添加到场景
//添加一个全局环境光
scene.add(new THREE.AmbientLight(0x222222));
}
上面我们添加了一个模拟太阳光线的平行光和一个对每一个物理都造成影响的环境光,具体的内容将会在下一篇讲解。
下面介绍的材质都是对光有反应的,而且如果场景内没有光,模型将无法显示。
MeshLambertMaterial 兰伯特材质
这种材质会对光有反应,但是不会出现高光,可以模拟一些粗糙的材质的物体,比如木头或者石头。
实现案例,如下:
1
2
3
4
5
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshLambertMaterial({color:0x00ffff}); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
MeshPhongMaterial 高光材质
这种材质具有高光效果,可以模拟一些光滑的物体的材质效果,比如油漆面,瓷瓦等光滑物体。
实现案例如下:
1
2
3
4
5
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshPhongMaterial({color:0x00ffff}); //创建材质
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
MeshStandardMaterial 基于物理的渲染(PBR)材质
这种材质基于物理的渲染(PBR)材质,生成的材质效果更佳,但是相应也占用更多的计算量。这种材质我们可以定义它的粗糙度来确定反光效果,经常用于模拟金属的质感,使金属质感更加真实。
实现案例如下:
1
2
3
4
5
6
7
geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体
material = new THREE.MeshPhongMaterial({color:0x00ffff}); //创建材质
material.metalness = 0.1; //设置的值的范围为0-1,值越小,材质越光滑,高光越明显
material.metalnessMap = 0.1; //设置的值的范围为0-1,值越大,越有生锈金属的质感,值越小反光越清晰
mesh = new THREE.Mesh( geometry, material ); //创建网格
scene.add( mesh ); //将网格添加到场景
案例代码
相机Camera
相机是 Three.js 抽象出来的一个对象,使用此对象,我们可以定义显示的内容,并且可以通过移动相机位置来显示不同的内容。
下面讲解一下 Three.js 中相机的通用属性和常用的相机对象。
相机通用属性和方法
我们常用的相机有正交相机(OrthographicCamera)和透视相机(PerspectiveCamera)两种,用于来捕获场景内显示的物体模型。它们有一些通用的属性和方法。
Object3D 的所有属性和方法
由于相机都继承自 THREE.Object3D 对象,所以像设置位置的 position 属性、rotation 旋转和 scale 缩放属性,可以直接对相机对象设置。我们甚至还可以使用 add()
方法,给相机对象添加子类,移动相机它的子类也会跟随着一块移动,我们可以使用这个特性制作一些比如 HUD 类型的显示界面。
target 焦点属性和 lookAt() 方法
这两个方法的效果一定,都是调整相机的朝向,可以设置一个 THREE.Vector3
(三维向量)点的位置:
1
2
camera.target = new THREE.Vector3(0, 0, 0);
camera.lookAt(new THREE.Vector3(0, 0, 0));
上面两个都是朝向了原点,我们也可以将相机的朝向改为模型网格的 position,如果物体的位置发生了变化,相机的焦点方向也会跟随变动:
1
2
3
4
var mesh = new THREE.Mesh(geometry, material);
camera.target = mesh.position;
//或者
camera.lookAt(mesh.position);
getWorldDirection()
getWorldDirection() 方法可以获取当前位置到 target 位置的世界中的方向。方向也可以使用 THREE.Vector3
对象表示,所以该方法返回一个三维向量。
OrthographicCamera 正交相机
使用正交相机 OrthographicCamera 渲染出来的场景,所有的物体和模型都按照它固有的尺寸和精度显示,一般使用在工业要求精度或者 2D 平面中,因为它能完整的显示物体应有的尺寸。
创建正交相机

上面的图片可以清晰的显示出正交相机显示的范围,它显示的内容是一个立方体结构,通过图片我们发现,只要确定 top、left、right、bottom、near 和 far 六个值,我们就能确定当前相机捕获场景的区域,在这个区域外面的内容不会被渲染,所以,我们创建相机的方法就是:
1
new THREE.OrthographicCamera( left, right, top, bottom, near, far );
下面我们创建了一个显示场景中相机位置前方长宽高都为4的盒子内的物体的正交相机:
1
2
var orthographicCamera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0, 4);
scene.add(orthographicCamera); //一般不需要将相机放置到场景当中,如果需要添加子元素等一些特殊操作,还是需要add到场景内
正常情况相机显示的内容需要和窗口显示的内容为同样的比例才能够显示没有被拉伸变形的效果:
1
2
3
var frustumSize = 1000; //设置显示相机前方1000高的内容
var aspect = window.innerWidth / window.innerHeight; //计算场景的宽高比
var orthographicCamera = new THREE.OrthographicCamera( frustumSize * aspect / - 2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / - 2, 1, 2000 ); //根据比例计算出left,top,right,bottom的值
动态修改正交相机属性
我们也可以动态的修改正交相机的一些属性,注意修改完以后需要调用相机 updateProjectionMatrix() 方法来更新相机显存里面的内容,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
var frustumSize = 1000; //设置显示相机前方1000高的内容
var aspect = window.innerWidth / window.innerHeight; //计算场景的宽高比
var orthographicCamera = new THREE.OrthographicCamera(); //实例化一个空的正交相机
orthographicCamera.left = frustumSize * aspect / - 2; //设置left的值
orthographicCamera.right = frustumSize * aspect / 2; //设置right的值
orthographicCamera.top = frustumSize / 2; //设置top的值
orthographicCamera.bottom = frustumSize / - 2; //设置bottom的值
orthographicCamera.near = 1; //设置near的值
orthographicCamera.far = 2000; //设置far的值
//注意,最后一定要调用updateProjectionMatrix()方法更新
orthographicCamera.updateProjectionMatrix();
窗口变动后的更新
由于浏览器的窗口可以随意修改,我们有时候需要监听浏览器窗口的变化,然后获取到最新的宽高比,再重新设置相关属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
var aspect = window.innerWidth / window.innerHeight; //重新获取场景的宽高比
//重新设置left right top bottom 四个值
orthographicCamera.left = frustumSize * aspect / - 2; //设置left的值
orthographicCamera.right = frustumSize * aspect / 2; //设置right的值
orthographicCamera.top = frustumSize / 2; //设置top的值
orthographicCamera.bottom = frustumSize / - 2; //设置bottom的值
//最后,记得一定要更新数据
orthographicCamera.updateProjectionMatrix();
//显示区域尺寸变了,我们也需要修改渲染器的比例
renderer.setSize(window.innerWidth, window.innerHeight);
PerspectiveCamera 透视相机
透视相机是最常用的也是模拟人眼视角的一种相机,它所渲染生成的页面是一种近大远小的效果。
创建透视相机

上面的图片就是一个透视相机的生成原理,我们先看看渲染的范围是如何生成的:
- 首先,我们需要确定一个 fov 值,这个值是用来确定相机前方的垂直视角,角度越大,我们能够查看的内容就越多。
- 然后,我们又确定了一个渲染的宽高比,这个宽高比最好设置成页面显示区域的宽高比,这样我们查看生成画面才不会出现拉伸变形的效果,这时,我们可以确定前面生成内容的范围是一个四棱锥的区域。
- 最后,我们需要确定的就是相机渲染范围的最小值 near 和最大值 far,注意,这两个值都是距离相机的距离,确定完数值后,相机会显示的范围就是一个近小远大的四棱柱的范围,我们能够看到的内容都是在这个范围内的。
通过上面的原理,我们需要设置 fov 垂直角度,aspect 视角宽高比例和 near 最近渲染距离 far 最远渲染距离,就能确定当前透视相机的渲染范围。
下面代码展示了一个透视相机的创建方法:
1
2
var perspectiveCamera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
scene.add( perspectiveCamera );
我们设置了前方的视角为45度,宽度和高度设置成显示窗口的宽度除以高度的比例即可,显示距离为1到1000距离以内的物体。
动态修改透视相机的属性
透视相机的属性创建完成后我们也可以根据个人需求随意修改,但是注意,相机的属性修改完成后,以后要调用 updateProjectionMatrix()
方法来更新:
1
2
3
4
5
6
7
8
9
10
11
var perspectiveCamera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
scene.add( perspectiveCamera );
//下面为修改当前相机属性
perspectiveCamera.fov = 75; //修改相机的fov
perspectiveCamera.aspect = window.innerWidth/window.innerHeight; //修改相机的宽高比
perspectiveCamera.near = 100; //修改near
perspectiveCamera.far = 500; //修改far
//最后更新
perspectiveCamera.updateProjectionMatrix();
显示窗口变动后的回调
如果当前场景浏览器的显示窗口变动了,比如修改了浏览器的宽高后,我们需要设置场景自动更新,下面是一个常用的案例:
1
2
3
4
5
6
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight; //重新设置宽高比
camera.updateProjectionMatrix(); //更新相机
renderer.setSize(window.innerWidth, window.innerHeight); //更新渲染页面大小
}
window.onresize = onWindowResize;
最后,我写了一个案例来查看透视相机和正交相机的显示区别:点击这里案例 Demo。
左侧是透视相机显示的效果,这种更符合人眼看到的效果,场景更加的立体。而右侧则是正交相机实现的效果,渲染出来的数值更加准确,但是却不符合人眼查看的效果。
案例代码查看地址:点击这里。
制作相机控制器
在实际生产当中,很多时候我们需要切换相机的位置,以达到项目需求。 Three.js 也有很多相机控制插件,这个我们会在后面的课程当中讲解。
下面是我书写的一个小案例,能够通过鼠标左键拖拽界面让相机围绕模型周围查看模型。
首先,先上案例地址:点击这里。
代码查看地址:点击这里。
其实和以前的代码相比,我们也只是多出来了一个 initControl 方法,在这个方法里面绑定鼠标事件。实现的思路也很简单:
- 首先绑定鼠标按下事件,获取到按下时的相机位置和距离原点的距离,计算出来相对于 Z 轴正方向的偏移。绑定鼠标移动事件。
- 然后,在鼠标移动事件里面获取到距离鼠标按下的偏移,通过鼠标偏移数值计算出现在相机位置,并赋值。
思路就这么简单,虽然实现起来会麻烦,我也尽量写的简单,大家尽量能够自己也写出来。
粒子Points
我们将学习到 Sprite 精灵和 Points 粒子,这两种对象共同点就是我们通过相机查看它们时,始终看到的是它们的正面,它们总朝向相机。通过它们的这种特性,我们可以实现广告牌的效果,或实现更多的比如雨雪、烟雾等更加绚丽的特效。
Sprite 精灵
精灵由于一直正对着相机的特性,一般使用在模型的提示信息当中。通过 THREE.Sprite
创建生成,由于 THREE.Sprite
和 THREE.Mesh
都属于 THREE.Object3D
的子类,所以,我们操作模型网格的相关属性和方法大部分对于精灵都适用。和精灵一起使用的还有一个 THREE.SpriteMaterial
对象,它专门配合精灵的材质。注意,精灵没有阴影效果。
下面首先创建一个最简单的精灵:
1
2
3
var spriteMaterial = new THREE.SpriteMaterial( { color: 0xffffff } );
var sprite = new THREE.Sprite( spriteMaterial );
scene.add( sprite );
创建一个带有纹理图片的精灵:
1
2
3
4
var spriteMap = new THREE.TextureLoader().load( "sprite.png" );
var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff } );
var sprite = new THREE.Sprite( spriteMaterial );
scene.add( sprite );
直接使用 canvas 创建精灵:
1
2
3
4
var spriteMap = new THREE.Texture(canvas);
var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff } );
var sprite = new THREE.Sprite( spriteMaterial );
scene.add( sprite );
相关属性
精灵特有的属性就一个,即 center 属性,值是一个二维向量 THREE.Vector2
,这个属性意义就是当前设置的精灵位置点的位置处于精灵图中的位置。如果 center 的值是 0,0
旋转的位置就在左下角,如果值为 1,1
的话,那旋转的位置则在精灵图的右上角,默认值是 0.5,0.5
:
1
sprite.center.set(0.5, 0); //设置位置点处于精灵的最下方中间位置
接下来,我们查看一下精灵的案例:点击这里。
案例代码查看地址:点击这里。
案例效果从左到右依次是,普通的精灵,贴图纹理的精灵和 canvas
创建的精灵。
points 粒子
粒子和精灵的效果是一样的,它们之间的区别是,如果当前场景内的精灵过多的话,就会出现性能问题。粒子的作用就是为解决很多精灵而出现的,我们可以使用粒子去模型数量很多的效果,比如下雨,下雪等,数量很多的时候就适合使用粒子来创建,相应的,提高性能的损失就是失去了对单个精灵的操作,所有的粒子的效果都是一样。总的来说,粒子就是提高性能减少的一些自由度,而精灵就是为了自由度而损失了一些性能。
粒子的创建
粒子 THREE.Points
和精灵 THREE.Sprite
还有网格 THREE.Mesh
都属于 THREE.Object3D
的一个扩展,但是粒子有一些特殊的情况就是 THREE.Points
是它们粒子个体的父元素,它的位置设置也是基于 THREE.Points
位置而定位,而修改 THREE.Points
的 scale 属性只会修改掉粒子个体的位置。下面我们看下一个粒子的创建方法。创建一个粒子,需要一个含有顶点的几何体,和粒子纹理 THREE.PointsMaterial
的创建:
1
2
3
4
5
//球体
var sphereGeometry = new THREE.SphereGeometry(5, 24, 16);
var sphereMaterial = new THREE.PointsMaterial({color: 0xff00ff});
var sphere = new THREE.Points(sphereGeometry, sphereMaterial);
scene.add(sphere);
上面是通过球体几何体创建的一个最简单的粒子特效。
使用任何几何体都可以,甚至自己生成的几何体都可以,比如创建星空的案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
var starsGeometry = new THREE.Geometry();
//生成一万个点的位置
for (var i = 0; i < 10000; i++) {
var star = new THREE.Vector3();
//THREE.Math.randFloatSpread 在区间内随机浮动* - 范围 / 2 *到* 范围 / 2 *内随机取值。
star.x = THREE.Math.randFloatSpread(2000);
star.y = THREE.Math.randFloatSpread(2000);
star.z = THREE.Math.randFloatSpread(2000);
starsGeometry.vertices.push(star);
}
var starsMaterial = new THREE.PointsMaterial({color: 0x888888});
var starField = new THREE.Points(starsGeometry, starsMaterial);
scene.add(starField);
使用一个空的几何体,将自己创建的顶点坐标放入,也可以实现一组粒子的创建。如果我们需要单独设置每一个粒子的颜色,可以给 geometry 的 colors 数组添加相应数量的颜色:
1
2
3
4
5
6
7
8
9
10
for (var i = 0; i < 10000; i++) {
var star = new THREE.Vector3();
//THREE.Math.randFloatSpread 在区间内随机浮动* - 范围 / 2 *到* 范围 / 2 *内随机取值。
star.x = THREE.Math.randFloatSpread(2000);
star.y = THREE.Math.randFloatSpread(2000);
star.z = THREE.Math.randFloatSpread(2000);
starsGeometry.vertices.push(star);
starsGeometry.colors.push(new THREE.Color("rgb("+Math.random()*255+", "+Math.random()*255+", "+Math.random()*255+")")); //添加一个随机颜色
}
THREE.PointsMaterial 粒子的纹理
如果我们需要设置粒子的样式,还是需要通过设置 THREE.PointsMaterial
属性实现:
1
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff}); //设置了粒子纹理的颜色
我们还可以通过 PointsMaterial 的 size 属性设置粒子的大小:
1
2
3
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, size:4}); //粒子的尺寸改为原来的四倍
//或者直接设置属性
pointsMaterial.size = 4;
我们也可以给粒子设置纹理:
1
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, map:texture}); //添加纹理
默认粒子是不受光照影响的,我们可以设置 lights 属性为 true,让粒子受光照影响:
1
2
3
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, lights:true});
//或者
pointsMaterial.lights = true; //开启受光照影响
我们也可以设置粒子不受到距离的影响产生近大远小的效果:
1
2
3
var pointsMaterial = new THREE.PointsMaterial({color: 0xff00ff, sizeAttenuation: false});
//或者
pointsMaterial.sizeAttenuation = false; //关闭粒子的显示效果受距离影响
粒子的效果就介绍到这里,希望大家熟练了以后能够做出来各种各样的花哨效果。
接下来展示下我给大家准备的粒子案例:点击这里。
代码查看地址:点击这里。
这个案例和精灵的案例区别就是,将球体改成了粒子,然后将立方体修改成了带有 canvas 纹理的粒子,并且在背景里面添加了一万个粒子
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9364fbab.html b/posts/9364fbab.html
index 5c15963c..41df12a2 100644
--- a/posts/9364fbab.html
+++ b/posts/9364fbab.html
@@ -1 +1 @@
-JavaScript面向对象编程总结 - AIGISSS JavaScript面向对象编程总结
本文最后更新于:1 年前
What is Object-oriented Programming
OOP是一种编程范例,或者编程风格,这是围绕对象而不是函数面向对象编程中的四个核心概念 `Encapsulation`---封装 `Abstraction`---抽象 `Inheritance`---继承 `Polymorphism`---多态 区别与面向过程编程
改变了其中一个函数,然后其他几个函数可能就奔溃了,这就是我们说的意大利面条代码。 函数之间深层次的关联变成了各种问题的来源,OOP就应运而生。
OOP 就一组相关的变量和函数组成合成一个单元,我们称之为对象(object)。把里面的函数称为方法,里面的变量称之为属性。
最好的函数是那些没有参数的函数,参数个数越少,使用和维护就越简单。这就是封装!
多态意味着多种形态
使用封装重新组合的相关的变量和函数,这样可以减少复杂性,可以在程序的不同部分重用这些对象 或者在不同程序中,通过抽象,隐藏细节和复杂性,只显示必要性,这种技术降低了复杂性,也隔离了代码更改的影响。 继承让我们消除多余的代码 多态性可以避免写出复杂丑陋的选择性代码
原型与原型继承
原型Prototypes
和 原型继承Prototyical Inheritance
JavaScript 中的类并不同于 Java 或者 c#中的类,因为 Javascript 是动态语言,所以类的本质上是更像是为了配合原型和原型继承所采取的必要的技术。
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
//使用字面量创建对象
const circle = {};
//一个Javascript的对象实际上是一组键值对的集合
//使用字面量语法来创建多个对象是有问题的,那就是对象的行为性,就像人一样可以做很多事就叫做行为性。
//解决方法就是用工厂函数(factory)或者构造函数(constructor)
//工厂函数
function createCircle(radius) {
return {
radius,
draw() {
console.log("draw");
}
};
}
const circle2 = createCircle(1);
// 构造函数
function Circle(radius) {
this.radius = raius;
this.draw = function() {
console.log("draw");
};
}
const circle3 = new Circle(2);
//当我们使用new操作符调用一个函数时,3件事发生了
//首先new操作符创建了一个空对象,然后设置this指向这个对象,最后返回这个对象
补充:
字面量是变量的字符串表示形式。它不是一种值,而是一种变量记法。
1
2
3
4
const a = 1; //1是字面量
const b = "hello world"; //hello world是字面量
const c = [1, 2, 3]; //[1,2,3]是字面量
const d = { foo: "bar" }; //{"foo":"bar"}是字面量
每个对象都有构造函数属性
这个属性引用了用来创建这个对象的构造函数
1
2
3
4
new String(); // ''," ",``
new Boolean(); // true ,false
new Number(); //1,2,3,4,5,6
new Object(); //{}

值类型复制值
对象或者引用类型复制他们的引用
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
let number=10;
function increase(number){
number++;
}
increase(number);
console.log(number) //10
let object={value:10};
function increase(object){
object.value++;
}
increase(object);
console.log(object) //{value:11}
````
不知道要访问的对象名称属性,是在运行时产生的,可以使用方括号的语法,或者属性名不符合命名规则时。
抽象意味着我们应该隐藏细节和复杂部分,只显示或者暴露必要的部分
```js
this.defaultLocaltion={x:0,y:1} // ====> let defaultLocaltion={x:0,y:1}
Object.defineProperty(this,'defaultLocaltion',{
get(){
return defaultLocaltion
},
set(value){
defaultLocaltion=value
}
})
Javascript 中没有类,只有对象,那只有对象的时候如何引入继承?答案是原型。 原型可以理解为一个对象的父母,原型就是一般的对象。1
2
3
4
5
6
7
8
const person = { name: "hello" };
Object.defineProperty(person, "name", {
writable: false,
enumerable: true,
configurable: false
});
delete person.name;
console.log(person); // { name: 'hello' }
获得对象原型的方法是调用 Object 对象的 getPrototypeOf 方法
1
2
3
4
5
6
function Circle(radius) {
this.radius = radius;
}
const circle = new Circle(1);
Circle.prototype; //这是构造函数创建的对象的父母真身
circle.__proto__ === Circle.prototype; // true
Object.keys 只返回实例的成员
for-in 循环返回所有的成员,对象实例本身的和它的原型的

在 Javascript 中,有个函数可以从给定的原型创建对象,就是 Object.create(第一个参数是用作创建的原型)
Javascript 里每个对象都有一个构造函数属性,能返回用以创建这个对象的构造函数
避免创建层级式继承关系,因为这十分脆弱。如果要用继承特性,最好维持在一级。好的组合胜过继承。
Object.assign()可以用这个方法从一个对象拷贝所有成员到另外一个对象
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
const canEat = {
eat: function() {
console.log("eating");
}
};
const canWalk = {
walk: function() {
console.log("eating");
}
};
const canSwin = {
swin: function() {
console.log("swining");
}
};
// const person = Object.assign({}, canEat, canWalk);
// 空对象实际上变成了2个对象的组合
// console.log(person);
function mixins(target, ...sources) {
Object.assign(target.prototype, ...sources);
}
// function Person() {}
// Object.assign(Person.prototype, canEat, canWalk);
// console.log(new Person());
function Dog() {}
mixins(Dog, canEat, canWalk);
console.log(new Dog());
function GoldFish() {}
mixins(GoldFish, canEat, canSwin);
console.log(new GoldFish());
ES6
函数声明 funciton sayHello(){}
结尾不需要加分号,函数声明是置顶的。
函数表达式const sayGoodbye=function(){}
结尾需要加分号,不会被置顶。
不同于函数,类声明和类表达式都不会被置顶
实例方法和静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle {
constructor(radius) {
this.radius = radius;
}
// Instance Method
draw() {}
// Static Mthod
static parse(str) {
const { radius } = JSON.parse(str);
return new Circle(radius);
}
}
const circle = Circle.parse('{"radius":1}');
console.log("Go: circle", circle); // Go: circle Circle { radius: 1 }
所以我们用静态方法的方式创建不属于具体实例的工具函数
1
2
3
4
5
6
const c = new Circle(2);
// Method call
c.draw(); // Circle { radius: 2 }
const draw = c.draw;
// Function call
draw(); // undefined
ES6 私有
第一种是用在命名的时候加下划线
第二种是使用 Symbol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _radius = Symbol();
const _draw = Symbol();
// Symbol() 是一个函数,能创建一个Symbol,这个不是构造函数,不能在前面加new修饰符,这样会报错
class Circle {
constructor(radius) {
this[_radius] = radius;
}
[_draw]() {
// 计算生成属性
}
}
const c = new Circle(1);
const key = Object.getOwnPropertySymbols(c)[0];
console.log(c[key]); // 1
第三种是使用 WeakMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const _radius = new WeakMap();
const _move = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
_move.set(this, () => {
// 箭头函数将从调用它的构造器继承过来,在这个构造器里。this是circle对象实例的引用。
// 当我们在构造器函数里使用箭头函数时,this不会重新绑定,也不会重设,直接从构造器继承
console.log("moving", this);
});
}
draw() {
_move.get(this)();
console.log("drawing...");
}
}
const c = new Circle(2);
c.draw();
getter&&setter
方法重写
1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
move() {
console.log("moving....");
}
}
class Circle extends Shape {
move() {
super.move();
console.log("circle move");
}
}
const c = new Circle();

AMD,也就是异步模块定义,主要是在浏览器程序中使用。

CommonJS
class Circle{
}
module.exports.Circle=Circle
只需要引入一个模块时,可以简化代码module.exports=Circle
引入时,使用 require 函数。
所以时 CommonJS 定义了 require 函数和 module 函数,这是 CommonJS 当中的语法。
ES6
export && import
在模块化之前,要记住一个首要原则,高度关联的东西应该放在一起。就好比在厨房放置了杯子盘子勺子等餐具,不应该把衣服存放在厨房,这就是高度关联。这就是编程中说的 Cohesion(内聚)。

Webpack
- npm i -g webpack-cli
- webpack-cli init
- npm init —yes
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+JavaScript面向对象编程总结 - AIGISSS JavaScript面向对象编程总结
本文最后更新于:1 年前
What is Object-oriented Programming
OOP是一种编程范例,或者编程风格,这是围绕对象而不是函数面向对象编程中的四个核心概念 `Encapsulation`---封装 `Abstraction`---抽象 `Inheritance`---继承 `Polymorphism`---多态 区别与面向过程编程
改变了其中一个函数,然后其他几个函数可能就奔溃了,这就是我们说的意大利面条代码。 函数之间深层次的关联变成了各种问题的来源,OOP就应运而生。
OOP 就一组相关的变量和函数组成合成一个单元,我们称之为对象(object)。把里面的函数称为方法,里面的变量称之为属性。
最好的函数是那些没有参数的函数,参数个数越少,使用和维护就越简单。这就是封装!
多态意味着多种形态
使用封装重新组合的相关的变量和函数,这样可以减少复杂性,可以在程序的不同部分重用这些对象 或者在不同程序中,通过抽象,隐藏细节和复杂性,只显示必要性,这种技术降低了复杂性,也隔离了代码更改的影响。 继承让我们消除多余的代码 多态性可以避免写出复杂丑陋的选择性代码
原型与原型继承
原型Prototypes
和 原型继承Prototyical Inheritance
JavaScript 中的类并不同于 Java 或者 c#中的类,因为 Javascript 是动态语言,所以类的本质上是更像是为了配合原型和原型继承所采取的必要的技术。
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
//使用字面量创建对象
const circle = {};
//一个Javascript的对象实际上是一组键值对的集合
//使用字面量语法来创建多个对象是有问题的,那就是对象的行为性,就像人一样可以做很多事就叫做行为性。
//解决方法就是用工厂函数(factory)或者构造函数(constructor)
//工厂函数
function createCircle(radius) {
return {
radius,
draw() {
console.log("draw");
}
};
}
const circle2 = createCircle(1);
// 构造函数
function Circle(radius) {
this.radius = raius;
this.draw = function() {
console.log("draw");
};
}
const circle3 = new Circle(2);
//当我们使用new操作符调用一个函数时,3件事发生了
//首先new操作符创建了一个空对象,然后设置this指向这个对象,最后返回这个对象
补充:
字面量是变量的字符串表示形式。它不是一种值,而是一种变量记法。
1
2
3
4
const a = 1; //1是字面量
const b = "hello world"; //hello world是字面量
const c = [1, 2, 3]; //[1,2,3]是字面量
const d = { foo: "bar" }; //{"foo":"bar"}是字面量
每个对象都有构造函数属性
这个属性引用了用来创建这个对象的构造函数
1
2
3
4
new String(); // ''," ",``
new Boolean(); // true ,false
new Number(); //1,2,3,4,5,6
new Object(); //{}

值类型复制值
对象或者引用类型复制他们的引用
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
let number=10;
function increase(number){
number++;
}
increase(number);
console.log(number) //10
let object={value:10};
function increase(object){
object.value++;
}
increase(object);
console.log(object) //{value:11}
````
不知道要访问的对象名称属性,是在运行时产生的,可以使用方括号的语法,或者属性名不符合命名规则时。
抽象意味着我们应该隐藏细节和复杂部分,只显示或者暴露必要的部分
```js
this.defaultLocaltion={x:0,y:1} // ====> let defaultLocaltion={x:0,y:1}
Object.defineProperty(this,'defaultLocaltion',{
get(){
return defaultLocaltion
},
set(value){
defaultLocaltion=value
}
})
Javascript 中没有类,只有对象,那只有对象的时候如何引入继承?答案是原型。 原型可以理解为一个对象的父母,原型就是一般的对象。1
2
3
4
5
6
7
8
const person = { name: "hello" };
Object.defineProperty(person, "name", {
writable: false,
enumerable: true,
configurable: false
});
delete person.name;
console.log(person); // { name: 'hello' }
获得对象原型的方法是调用 Object 对象的 getPrototypeOf 方法
1
2
3
4
5
6
function Circle(radius) {
this.radius = radius;
}
const circle = new Circle(1);
Circle.prototype; //这是构造函数创建的对象的父母真身
circle.__proto__ === Circle.prototype; // true
Object.keys 只返回实例的成员
for-in 循环返回所有的成员,对象实例本身的和它的原型的

在 Javascript 中,有个函数可以从给定的原型创建对象,就是 Object.create(第一个参数是用作创建的原型)
Javascript 里每个对象都有一个构造函数属性,能返回用以创建这个对象的构造函数
避免创建层级式继承关系,因为这十分脆弱。如果要用继承特性,最好维持在一级。好的组合胜过继承。
Object.assign()可以用这个方法从一个对象拷贝所有成员到另外一个对象
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
const canEat = {
eat: function() {
console.log("eating");
}
};
const canWalk = {
walk: function() {
console.log("eating");
}
};
const canSwin = {
swin: function() {
console.log("swining");
}
};
// const person = Object.assign({}, canEat, canWalk);
// 空对象实际上变成了2个对象的组合
// console.log(person);
function mixins(target, ...sources) {
Object.assign(target.prototype, ...sources);
}
// function Person() {}
// Object.assign(Person.prototype, canEat, canWalk);
// console.log(new Person());
function Dog() {}
mixins(Dog, canEat, canWalk);
console.log(new Dog());
function GoldFish() {}
mixins(GoldFish, canEat, canSwin);
console.log(new GoldFish());
ES6
函数声明 funciton sayHello(){}
结尾不需要加分号,函数声明是置顶的。
函数表达式const sayGoodbye=function(){}
结尾需要加分号,不会被置顶。
不同于函数,类声明和类表达式都不会被置顶
实例方法和静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle {
constructor(radius) {
this.radius = radius;
}
// Instance Method
draw() {}
// Static Mthod
static parse(str) {
const { radius } = JSON.parse(str);
return new Circle(radius);
}
}
const circle = Circle.parse('{"radius":1}');
console.log("Go: circle", circle); // Go: circle Circle { radius: 1 }
所以我们用静态方法的方式创建不属于具体实例的工具函数
1
2
3
4
5
6
const c = new Circle(2);
// Method call
c.draw(); // Circle { radius: 2 }
const draw = c.draw;
// Function call
draw(); // undefined
ES6 私有
第一种是用在命名的时候加下划线
第二种是使用 Symbol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _radius = Symbol();
const _draw = Symbol();
// Symbol() 是一个函数,能创建一个Symbol,这个不是构造函数,不能在前面加new修饰符,这样会报错
class Circle {
constructor(radius) {
this[_radius] = radius;
}
[_draw]() {
// 计算生成属性
}
}
const c = new Circle(1);
const key = Object.getOwnPropertySymbols(c)[0];
console.log(c[key]); // 1
第三种是使用 WeakMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const _radius = new WeakMap();
const _move = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
_move.set(this, () => {
// 箭头函数将从调用它的构造器继承过来,在这个构造器里。this是circle对象实例的引用。
// 当我们在构造器函数里使用箭头函数时,this不会重新绑定,也不会重设,直接从构造器继承
console.log("moving", this);
});
}
draw() {
_move.get(this)();
console.log("drawing...");
}
}
const c = new Circle(2);
c.draw();
getter&&setter
方法重写
1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
move() {
console.log("moving....");
}
}
class Circle extends Shape {
move() {
super.move();
console.log("circle move");
}
}
const c = new Circle();

AMD,也就是异步模块定义,主要是在浏览器程序中使用。

CommonJS
class Circle{
}
module.exports.Circle=Circle
只需要引入一个模块时,可以简化代码module.exports=Circle
引入时,使用 require 函数。
所以时 CommonJS 定义了 require 函数和 module 函数,这是 CommonJS 当中的语法。
ES6
export && import
在模块化之前,要记住一个首要原则,高度关联的东西应该放在一起。就好比在厨房放置了杯子盘子勺子等餐具,不应该把衣服存放在厨房,这就是高度关联。这就是编程中说的 Cohesion(内聚)。

Webpack
- npm i -g webpack-cli
- webpack-cli init
- npm init —yes
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604abcd.html b/posts/9604abcd.html
index 443ad0fb..fbbb1678 100644
--- a/posts/9604abcd.html
+++ b/posts/9604abcd.html
@@ -1 +1 @@
-geoserver发布矢量切片说明 - AIGISSS geoserver发布矢量切片说明
本文最后更新于:6 个月前
1、安装postgresql/postgis:

安装注意:先安装postgresql再安装postgis,注意用户名默认是postgres不要改动,密码需要安装者设置,用于后续数据库的登录,过程中一直点下一步即可,注意安装完postgresql弹出下图窗口后直接关闭!

安装postgis最后会弹出类似下图窗口,直接都点是

最终安装成功!
2、新建数据库

鼠标单击选中新建的数据库,进去sql语句命名窗口:

增加postgis扩展,sql语句为 create extension postgis,具体步骤见下图:

3、使用postgis桌面端工具将shp数据导入postgresql
打开postgis shapefile import/export manager,准备导入shp数据:

如果使用上述中国osm数据的话直接全选所有shp文件,点击open后,再点击import即可,需要一些时间等待导入完成,最终导入成功会有successed信息!

4、安装Geoserver
网盘中获取安装包,先装jdk,安装完成后在安装的目录bin里面启动geoserver,如图位置:

5、增加geoserver的矢量切片插件
下载矢量切片插件geoserver-2_14_2-vectortiles-plugin.rar,解压缩后将jar包拷贝到下图路径下面:

6、geoserver主界面
打开geoserver界面 http://ip:port(默认8080)/geoserver,用户名和密码默认是geoserver/admin
7、新建工作区:
注意工作区名称命名为chinaosm,后续需要发布的图层均在此新建工作区下完成,因此发布新图层的时候注意选择到正确的工作区!

8、新建数据存储:

9、新建图层:

注意下图列表为====》工作空间名称:数据源名称

10、新建图层组:
新建图层组,作用是将之前新建的工作区中的所有新建图层放置到图层组中,进行统一管理与矢量切片的发布,注意图层组命名为chinaOSM,注意选择需要添加的工作区,添加所有图层!

自此发布矢量切片服务完成!测试可在前端mapbox页面进行测试!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+geoserver发布矢量切片说明 - AIGISSS geoserver发布矢量切片说明
本文最后更新于:1 分钟前
1、安装postgresql/postgis:

安装注意:先安装postgresql再安装postgis,注意用户名默认是postgres不要改动,密码需要安装者设置,用于后续数据库的登录,过程中一直点下一步即可,注意安装完postgresql弹出下图窗口后直接关闭!

安装postgis最后会弹出类似下图窗口,直接都点是

最终安装成功!
2、新建数据库

鼠标单击选中新建的数据库,进去sql语句命名窗口:

增加postgis扩展,sql语句为 create extension postgis,具体步骤见下图:

3、使用postgis桌面端工具将shp数据导入postgresql
打开postgis shapefile import/export manager,准备导入shp数据:

如果使用上述中国osm数据的话直接全选所有shp文件,点击open后,再点击import即可,需要一些时间等待导入完成,最终导入成功会有successed信息!

4、安装Geoserver
网盘中获取安装包,先装jdk,安装完成后在安装的目录bin里面启动geoserver,如图位置:

5、增加geoserver的矢量切片插件
下载矢量切片插件geoserver-2_14_2-vectortiles-plugin.rar,解压缩后将jar包拷贝到下图路径下面:

6、geoserver主界面
打开geoserver界面 http://ip:port(默认8080)/geoserver,用户名和密码默认是geoserver/admin
7、新建工作区:
注意工作区名称命名为chinaosm,后续需要发布的图层均在此新建工作区下完成,因此发布新图层的时候注意选择到正确的工作区!

8、新建数据存储:

9、新建图层:

注意下图列表为====》工作空间名称:数据源名称

10、新建图层组:
新建图层组,作用是将之前新建的工作区中的所有新建图层放置到图层组中,进行统一管理与矢量切片的发布,注意图层组命名为chinaOSM,注意选择需要添加的工作区,添加所有图层!

自此发布矢量切片服务完成!测试可在前端mapbox页面进行测试!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2a5.html b/posts/9604e2a5.html
index 7e602d1b..6201a510 100644
--- a/posts/9604e2a5.html
+++ b/posts/9604e2a5.html
@@ -1 +1 @@
-给 Hexo 博客添加 PWA 支持 - AIGISSS 给 Hexo 博客添加 PWA 支持
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:吃白饭
原文地址:https://eatrice.top/post/给hexo博客添加PWA支持/
简介
PWA(Progressive Web App)的中文名叫做「渐进式网页应用」,早在2014年, W3C 公布过 Service Worker 的相关草案,但是其在生产环境被 Chrome 支持是在 2015 年。因此,如果我们把 PWA 的关键技术之一 Service Worker 的出现作为 PWA 的诞生时间,那就应该是 2015 年。
自 2015 年以来,PWA 相关的技术不断升级优化,在用户体验和用户留存两方面都提供了非常好的解决方案。PWA 可以将 Web 和 App 各自的优势融合在一起:渐进式、可响应、可离线、实现类似 App 的交互、即时更新、安全、可以被搜索引擎检索、可推送、可安装、可链接。[1]
由于 Hexo 为静态博客,因此不需要具备推送功能(其实是我没搞懂)。因此PWA的特性包括其渐进式、可离线,可以作为提高网站体验和提高网站家在速度的一个方法。因此下面将从其主要内容和hexo如何安装两个方面以“吃白饭的休伯利安号”为例来简单演示一遍安装过程。
内容
渐进式
什么是渐进式,即将传统的web应用,应用现代的技术和方法使之在能够有桌面应用一般的体验,即为渐进式web应用。渐进式web应用可以同时运行在传统的浏览器上,像普通的网站一样进行浏览和操作;其同时也可以运行在现代功能完善的浏览器中,可以使其具备更多的效果和功能。比较常见的有可安装,即在支持的浏览器和操作系统上可以生成访问图标,通过图标可以可桌面应用一样访问应用;消息推送,即访问应用时服务器端可以通过应用的后台进程主动向客户端推送消息,类似于桌面应用的消息队列。
可离线
支持应用离线访问,即正常访问应用时,后台进程会自动缓存内容,下次访问时应用优先从缓存区读取数据,然后是进行web请求。因此可离线实质上充当了web代理服务器的职责,先是将正常请求代理到缓存区,再是将缓存区不足的文件进行正常的网络请求,通过此方法实现了离线的目标。根据可离线的规律,应用在一次访问缓存之后二次访问即可断网。
安装
Web app manifest
首先要实现PWA的可安装性,需要有一个清单文件manifest.json
。manifest.json
是一个简单的json
文件,它描述了我们的图标在主屏幕上如何显示,以及图标点击进去的启动页是什么,自动生成manifest.json
的工具:manifest.json生成工具(需要梯子),本站的JSON格式如下所示:
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
{
"name": "吃白饭的休伯利安号",
"short_name": "吃白饭博客",
"theme_color": "#3a311c",
"background_color": "#3a311c",
"display": "standalone",
"Scope": "/",
"start_url": "/",
"icons": [
{
"src": "/images/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/images/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"splash_pages": null
}
其中:
- start_url 可以设置启动网址
- icons 可以设置各个分辨率下页面的图标,适配不同的尺寸的路径
- background_color 会设置背景颜色, Chrome 在网络应用启动后会立即使用此颜色,这一颜色将保留在屏幕上,直至网络应用首次呈现为止。
- theme_color 会设置主题颜色
- display 设置启动样式
配置好manifest.json
后进行调试,打开浏览器的控制台如下图所示,即文件配置成功。

离线使用
离线使用依赖Service Work
,其本质是一段运行在并行于主进程的后台进程上,他不参与web交互功能,主要职责是和服务器交互,和指示缓存的内容。其详细的生命周期和原理文档详见:Using Service Workers。可以通过文档中的生命周期对这段后台脚本进行深度开发。
Hexo 为静态博客,因此只需要实现离线使用即可,不需要进行消息推送,因此可以使用固定服务注册脚本,在 Hexo 中服务注册脚本有着专门的插件进行生成:
hexo-offline hexo-pwa hexo-service-worker Hexo 的离线插件不包括安装 百度出的PWA综合插件,支持同时生成manifest.json,有很多的配置项 和hexo-offline类似
三个插件的原理相同,通过注册SW服务,配合manifest.json
,文件达到可安装和可离线的功能,本站使用的是hexo-service-worker
插件,下面是插件使用的细节:
- 首先安装
hexo-service-worker
插件:
1
npm install --save hexo-service-worker
- 在 Hexo 的全局配置文件
config.yml
中添加配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# offline config passed to sw-precache.
service_worker:
maximumFileSizeToCacheInBytes: 5242880
staticFileGlobs:
- public/index.html
- public/img/favcion.png
- public/manifest.json
stripPrefix: public
verbose: false
runtimeCaching:
- urlPattern: /**/*
handler: cacheFirst
options:
origin: eatrice.top
其中
- maximumFileSizeToCacheInBytes 为最大缓存大小,字节数
- staticFileGlobs 关键的文件路径
- stripPrefix 网站文件的根路径绝对位置
- runtimeCaching 缓存选项
- urlPattern 文件的正则匹配
- handler 缓存模式
- origin 网站访问域名(代理域名)
如此支持离线的PWA即配置成功。若要使用其他两个插件进行配置可以参考:
然后执行生成发布。使用新版的chrome访问网站,打开控制台的Audits
点击生成报告,就能看到网站是否支持PWA啦,如下图所示:

发布之后可以先访问一下网站的一些页面,然后就可以拿把大剪子网线访问你的网站啦~
关于消息推送,还没太搞明白其中的原理,自己的博客也用不到,所以就不仔细讨论啦。
参考资料
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+给 Hexo 博客添加 PWA 支持 - AIGISSS 给 Hexo 博客添加 PWA 支持
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:吃白饭
原文地址:https://eatrice.top/post/给hexo博客添加PWA支持/
简介
PWA(Progressive Web App)的中文名叫做「渐进式网页应用」,早在2014年, W3C 公布过 Service Worker 的相关草案,但是其在生产环境被 Chrome 支持是在 2015 年。因此,如果我们把 PWA 的关键技术之一 Service Worker 的出现作为 PWA 的诞生时间,那就应该是 2015 年。
自 2015 年以来,PWA 相关的技术不断升级优化,在用户体验和用户留存两方面都提供了非常好的解决方案。PWA 可以将 Web 和 App 各自的优势融合在一起:渐进式、可响应、可离线、实现类似 App 的交互、即时更新、安全、可以被搜索引擎检索、可推送、可安装、可链接。[1]
由于 Hexo 为静态博客,因此不需要具备推送功能(其实是我没搞懂)。因此PWA的特性包括其渐进式、可离线,可以作为提高网站体验和提高网站家在速度的一个方法。因此下面将从其主要内容和hexo如何安装两个方面以“吃白饭的休伯利安号”为例来简单演示一遍安装过程。
内容
渐进式
什么是渐进式,即将传统的web应用,应用现代的技术和方法使之在能够有桌面应用一般的体验,即为渐进式web应用。渐进式web应用可以同时运行在传统的浏览器上,像普通的网站一样进行浏览和操作;其同时也可以运行在现代功能完善的浏览器中,可以使其具备更多的效果和功能。比较常见的有可安装,即在支持的浏览器和操作系统上可以生成访问图标,通过图标可以可桌面应用一样访问应用;消息推送,即访问应用时服务器端可以通过应用的后台进程主动向客户端推送消息,类似于桌面应用的消息队列。
可离线
支持应用离线访问,即正常访问应用时,后台进程会自动缓存内容,下次访问时应用优先从缓存区读取数据,然后是进行web请求。因此可离线实质上充当了web代理服务器的职责,先是将正常请求代理到缓存区,再是将缓存区不足的文件进行正常的网络请求,通过此方法实现了离线的目标。根据可离线的规律,应用在一次访问缓存之后二次访问即可断网。
安装
Web app manifest
首先要实现PWA的可安装性,需要有一个清单文件manifest.json
。manifest.json
是一个简单的json
文件,它描述了我们的图标在主屏幕上如何显示,以及图标点击进去的启动页是什么,自动生成manifest.json
的工具:manifest.json生成工具(需要梯子),本站的JSON格式如下所示:
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
{
"name": "吃白饭的休伯利安号",
"short_name": "吃白饭博客",
"theme_color": "#3a311c",
"background_color": "#3a311c",
"display": "standalone",
"Scope": "/",
"start_url": "/",
"icons": [
{
"src": "/images/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/images/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"splash_pages": null
}
其中:
- start_url 可以设置启动网址
- icons 可以设置各个分辨率下页面的图标,适配不同的尺寸的路径
- background_color 会设置背景颜色, Chrome 在网络应用启动后会立即使用此颜色,这一颜色将保留在屏幕上,直至网络应用首次呈现为止。
- theme_color 会设置主题颜色
- display 设置启动样式
配置好manifest.json
后进行调试,打开浏览器的控制台如下图所示,即文件配置成功。

离线使用
离线使用依赖Service Work
,其本质是一段运行在并行于主进程的后台进程上,他不参与web交互功能,主要职责是和服务器交互,和指示缓存的内容。其详细的生命周期和原理文档详见:Using Service Workers。可以通过文档中的生命周期对这段后台脚本进行深度开发。
Hexo 为静态博客,因此只需要实现离线使用即可,不需要进行消息推送,因此可以使用固定服务注册脚本,在 Hexo 中服务注册脚本有着专门的插件进行生成:
hexo-offline hexo-pwa hexo-service-worker Hexo 的离线插件不包括安装 百度出的PWA综合插件,支持同时生成manifest.json,有很多的配置项 和hexo-offline类似
三个插件的原理相同,通过注册SW服务,配合manifest.json
,文件达到可安装和可离线的功能,本站使用的是hexo-service-worker
插件,下面是插件使用的细节:
- 首先安装
hexo-service-worker
插件:
1
npm install --save hexo-service-worker
- 在 Hexo 的全局配置文件
config.yml
中添加配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# offline config passed to sw-precache.
service_worker:
maximumFileSizeToCacheInBytes: 5242880
staticFileGlobs:
- public/index.html
- public/img/favcion.png
- public/manifest.json
stripPrefix: public
verbose: false
runtimeCaching:
- urlPattern: /**/*
handler: cacheFirst
options:
origin: eatrice.top
其中
- maximumFileSizeToCacheInBytes 为最大缓存大小,字节数
- staticFileGlobs 关键的文件路径
- stripPrefix 网站文件的根路径绝对位置
- runtimeCaching 缓存选项
- urlPattern 文件的正则匹配
- handler 缓存模式
- origin 网站访问域名(代理域名)
如此支持离线的PWA即配置成功。若要使用其他两个插件进行配置可以参考:
然后执行生成发布。使用新版的chrome访问网站,打开控制台的Audits
点击生成报告,就能看到网站是否支持PWA啦,如下图所示:

发布之后可以先访问一下网站的一些页面,然后就可以拿把大剪子网线访问你的网站啦~
关于消息推送,还没太搞明白其中的原理,自己的博客也用不到,所以就不仔细讨论啦。
参考资料
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2a6.html b/posts/9604e2a6.html
index df40ee4e..a5e075f8 100644
--- a/posts/9604e2a6.html
+++ b/posts/9604e2a6.html
@@ -1,4 +1,4 @@
-使用 ECharts 插件绘制炫酷图表 - AIGISSS 使用 ECharts 插件绘制炫酷图表
本文最后更新于:6 个月前
ECharts使用
- 安装
hexo-tag-echarts
插件
1
$ npm install hexo-tag-echarts --save
注意:ECharts官网教程-5 分钟上手 ECharts)里的npm install echarts --save
并不适合hexo博客,这种安装方式无效,请安装hexo-tag-echarts
插件。
添加如下js文件
1
2
3
4
// 通过jsDelivr的CDN引入echarts
<script src="https://cdn.jsdelivr.net/npm/echarts@4.8.0/dist/echarts.min.js"></script>
// 使用GL里的各种组件时需要添加,否则可不需要
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@1.1.1/dist/echarts-gl.min.js"></script>
- 在markdown文件下添加echarts,格式如下
1
2
3
4
5
6
7
<script>
...
</script>
{% echarts 400 '85%' %}
...
{% endecharts %}
<script>
中添加定义的变量和函数,若无设定则可删掉<script></script>
{% echarts 400 '85%' %}
和{% endecharts %}
之间添加echarts的option
。- 参数400指定图表展示的高度为400px,85%则指定图表展示的宽度为85%,如不写明这两项参数则默认值为高度400px,宽度81%。
- title:标题组件,包含主标题和副标题。
- legend:图例组件。
- tooltip:提示框组件。
- toolbox:工具栏。内置有导出图片,数据视图,动态类型切换,数据区域缩放,重置五个工具。
- xAxis、yAxis:直角坐标系 grid 中的 x 轴、y轴。
- series:系列列表。每个系列通过
type
决定自己的图表类型。- series-line:折线/面积图
- series-bar:柱状/条形图
- series-pie:饼图
- series-scatter:散点图
- series-radar:雷达图
- series-tree:树图
- series-boxplot:箱形图
- series-candlestick:K线图
- series-heatmap:热力图
- series-graph:关系图
- 多个图表的数据和函数可能会冲突,请注意!
- 直接在html中直接绘制,然后用
<iframe></iframe>
展示效果更佳。关于hexo的html文件渲染问题,可以参考Fluid+自定义html,主要是去掉head
部分的说明。 - 在html绘图ECharts的格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script src="https://cdn.jsdelivr.net/npm/echarts@4.8.0/dist/echarts.min.js"></script>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
...
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
// 刷新调整
window.onresize = function () {
myChart.resize();
}
</script>
- 部分echart需要引入其他js,如
bmap
、jquery
等,请自行添加。 - 使用百度地图的api需要申请密钥(ak),使用格式如下,注意替换
FAKE_AK
。
1
2
<script type="text/javascript" src="https://api.map.baidu.com/api?v=2.0&ak=FAKE_AK"></script>
<script type="text/javascript" src="https://api.map.baidu.com/getscript?v=2.0&ak=FAKE_AK"></script>
实例
下面给出一些echarts官方实例,大多数都可以交互。
折线图Line
柱状图Bar
使用 ECharts 插件绘制炫酷图表
本文最后更新于:6 个月前
ECharts使用
- 安装
hexo-tag-echarts
插件
1
$ npm install hexo-tag-echarts --save
注意:ECharts官网教程-5 分钟上手 ECharts)里的npm install echarts --save
并不适合hexo博客,这种安装方式无效,请安装hexo-tag-echarts
插件。
添加如下js文件
1
2
3
4
// 通过jsDelivr的CDN引入echarts
<script src="https://cdn.jsdelivr.net/npm/echarts@4.8.0/dist/echarts.min.js"></script>
// 使用GL里的各种组件时需要添加,否则可不需要
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@1.1.1/dist/echarts-gl.min.js"></script>
- 在markdown文件下添加echarts,格式如下
1
2
3
4
5
6
7
<script>
...
</script>
{% echarts 400 '85%' %}
...
{% endecharts %}
<script>
中添加定义的变量和函数,若无设定则可删掉<script></script>
{% echarts 400 '85%' %}
和{% endecharts %}
之间添加echarts的option
。- 参数400指定图表展示的高度为400px,85%则指定图表展示的宽度为85%,如不写明这两项参数则默认值为高度400px,宽度81%。
- title:标题组件,包含主标题和副标题。
- legend:图例组件。
- tooltip:提示框组件。
- toolbox:工具栏。内置有导出图片,数据视图,动态类型切换,数据区域缩放,重置五个工具。
- xAxis、yAxis:直角坐标系 grid 中的 x 轴、y轴。
- series:系列列表。每个系列通过
type
决定自己的图表类型。- series-line:折线/面积图
- series-bar:柱状/条形图
- series-pie:饼图
- series-scatter:散点图
- series-radar:雷达图
- series-tree:树图
- series-boxplot:箱形图
- series-candlestick:K线图
- series-heatmap:热力图
- series-graph:关系图
- 多个图表的数据和函数可能会冲突,请注意!
- 直接在html中直接绘制,然后用
<iframe></iframe>
展示效果更佳。关于hexo的html文件渲染问题,可以参考Fluid+自定义html,主要是去掉head
部分的说明。 - 在html绘图ECharts的格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script src="https://cdn.jsdelivr.net/npm/echarts@4.8.0/dist/echarts.min.js"></script>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
...
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
// 刷新调整
window.onresize = function () {
myChart.resize();
}
</script>
- 部分echart需要引入其他js,如
bmap
、jquery
等,请自行添加。 - 使用百度地图的api需要申请密钥(ak),使用格式如下,注意替换
FAKE_AK
。
1
2
<script type="text/javascript" src="https://api.map.baidu.com/api?v=2.0&ak=FAKE_AK"></script>
<script type="text/javascript" src="https://api.map.baidu.com/getscript?v=2.0&ak=FAKE_AK"></script>
实例
下面给出一些echarts官方实例,大多数都可以交互。
折线图Line
柱状图Bar
gitlab-ci自动化部署
本文最后更新于:1 年前
前言:
此文章是公司同事培训时准备的资料
这里先为大家展示下 gitlab 与 gitlab-ci 的工作流程图,如下

环境准备
打包部署在同一虚拟机
1,虚拟机需要安装 gitlab-runner 服务;
2,安装 node 及 git 环境(如果 node 依赖有用到私仓,需要添加切换私仓镜像)
打包部署在不同虚拟机
1,打包虚拟机安装内容同 1.1;
2,部署虚拟机仅需安装 gitlab-runner 服务即可
gitlab-runner 安装步骤
1,在服务器 C 盘或其他盘符新建一个 GitlabRunner 的文件夹,将文档根目录下的gitlab-runner-windows-amd64.exe
程序拷贝到其中
2,在 GitlabRunner 文件夹目录下 shift+右键,“在此处打开命令窗口”
3,输入 gitlab-runner-windows-amd64.exe register
根据提示填写内容(如下图)

其中 token 是通过管理员登陆 gitlab 后,在 Runners 中复制过来,如下图

注意:tags 的配置可以配置多个,命令行中可以按照 description 的格式来写,后面 yml 文件中会用到
4,输入 gitlab-runner-windows-amd64.exe install
没有报错后再输入 gitlab-runner-windows-amd64.exe start
5,打开任务管理器查看 gitlab-runner 是否正在运行,如下图

6,最终自己登陆上 gitlab 后可以在 看到刚添加上的 gitlab-runner 的状态,如下图状态即为安装成功

部署流程
部署服务器上配置开启 web-server
常用 web-server 有 IIS 和 nginx,配置好后,开启 web 服务后,将存放 web 页面的文件路径记录好,如下
E:\web\autodeploy\
后面 yml 配置文件要用到
gitlab 建立仓储或者使用已有的仓储
这里已 vue 项目为例
1,本地打开项目代码,新建一个名为 .gitlab-ci.yml
文件在项目根目录(与 package.json 同级)
2,.gitlab-ci.yml
中添加配置,详见三
3,git push origin master
push 完成后,在 gitlab 仓储的 CI/CD 中的 Pipeline 中就可以看到部署进度(部署的时长与 npm install 的速度相关)所以建议减少高频 push 代码来触发自动化部署或者通过指定分支来做(在.gitlab-ci.yml 中解释如何操作)
4,push 到远端后,就可以在仓储 CI/CD 的 Pipelines 中看到这条流水线(如下图状态)

5,也可以点开上图红框按钮,进入到当前 job 中查看部署日志状态(如下图),完成后日志结尾会提示 Job succeeded

6,完成后,如下图状态,即为部署成功,这是就可以打开自己在 web-server 上配置的 ip 或者域名来访问网站(此处样例链接 http://autodeploy.rdapp/ )

.gitlab-ci.yml 文件配置解释
.gitlab-ci.yml 每步配置的解释,见下图:

.gitlab-ci.yml 源码,可直接复制,针对项目部署情况修改使用
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
stages:
- build
- deploy
# cache:
# key: "$CI_BUILD_REF_NAME"
# paths:
# - node_modules/
build_job:
stage: build
cache:
key: "$CI_BUILD_REF_NAME"
paths:
- node_modules/
script:
- chcp 65001
- cmd /c version.sh
- cmd /c npm install
- cmd /c npm run build
artifacts:
name: "%CI_COMMIT_REF_NAME%-%CI_COMMIT_SHORT_SHA%"
paths:
- dist/
tags:
- 9.66_Runner
# 部署与build使用相同runner使用下面配置,注释deploy2_job
# deploy1_job:
# stage: deploy
# variables:
# GIT_STRATEGY: none
# cache: {}
# dependencies: []
# script:
# - chcp 65001
# - xcopy dist C:\test\ /C/Q/E/Y
# 拷贝当前目录下所有文件夹及内容
# - xcopy . E:\test /C/Q/E/Y
# only:
# - master
# tags:
# - 9.66_Runner
# 部署与build使用不同runner使用下面配置,注释deploy1_job
deploy2_job:
stage: deploy
variables:
GIT_STRATEGY: none
# cache: {}
dependencies:
- build_job
script:
- chcp 65001
- xcopy dist E:\web\autodeploy\ /C/Q/E/Y
# only:
# - master
tags:
- 9.95_Runner
version.sh 文件配置解释
在第三部分中的.gitlab-ci.yml 代码中 build_job 的 script 中有这么一行命令 - cmd /c version.sh
这里通过执行 version.sh 这么一个 bash 命令文件来生成方便开发和测试的一种辅助功能,具体看下图

version.sh 代码如下
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
git rev-list HEAD | sort > config.git-hash
LOCALVER=`wc -l config.git-hash | awk '{print $1}'`
if [ $LOCALVER \> 1 ] ; then
REVISE=`git rev-list origin/master | sort | join config.git-hash - | wc -l | awk '{print $1}'`
VERSION=`git tag`
echo "$VERSION"
if [ $REVISE != $LOCALVER ] ; then
REVISE="$REVISE+$(($LOCALVER-$REVISE))"
fi
if git status | grep -q "modified:" ; then
REVISE="${REVISE}"
fi
if [ ! -n "$VERSION" ]; then
VERSION="v1.0.0"
else
VERSION=`git describe --tags $(git rev-list --tags --max-count=1)`
fi
GIT_HASH=`git rev-list HEAD -n 1`
GIT_SHORT_HASH=`git rev-list HEAD -n 1 | cut -c 1-8`
TIME=`git log --pretty=format:"%cd" $GIT_SHORT_HASH -1`
echo "$TIME"
else
GIT_VERSION=
REVISE="x"SHORT_
fi
rm -f config.git-hash
cat version.template | sed "s/\$FULL_HASH/$GIT_HASH/g" | sed "s/\$FULL_REVISE/$REVISE/g" | sed "s/\$FULL_VERSION/$VERSION/g" | sed "s/\$FULL_TIME/$TIME/g" > public/$REVISE
cat version.json.template | sed "s/\$FULL_HASH/$GIT_SHORT_HASH/g" | sed "s/\$FULL_REVISE/$REVISE/g" | sed "s/\$FULL_VERSION/$VERSION/g" | sed "s/\$FULL_TIME/$TIME/g" > public/version.json
echo "$VERSION.$REVISE"
echo "Generated version info : $REVISE"
version.template 代码如下:
1
2
3
4
Version $FULL_VERSION
Revise $FULL_REVISE
Git_hash $FULL_HASH
Git_time $FULL_TIME
version.json.template 代码如下:
1
2
3
4
5
6
{
"Tag_version": "$FULL_VERSION",
"Commit_count": "$FULL_REVISE",
"Current_commit_ID": "$FULL_HASH",
"Current_commit_time": "$FULL_TIME"
}
补充
因 build 环节,可能会出现不同项目有不同的 npm 仓,有的包需要用到私仓或者 npm 公仓;这里做以下补充
第四部分的介绍属于扩展,根据项目,按需加
下面介绍需要使用多不同的 npm 镜像时如何处理:
修改.gitlab-ci.yml 文件,如下:
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
stages:
- build
- deploy
build_job:
stage: build
script:
- chcp 65001
- cmd /c version.sh
- cmd /c build.sh
artifacts:
name: "%CI_COMMIT_REF_NAME%-%CI_COMMIT_SHORT_SHA%"
paths:
- dist/
tags:
- 9.101_Runner
# 部署与build使用相同runner使用下面配置,注释deploy2_job
# deploy1_job:
# stage: deploy
# variables:
# GIT_STRATEGY: none
# cache: {}
# dependencies: []
# script:
# - chcp 65001
# - xcopy dist C:\test\ /C/Q/E/Y
# only:
# - master
# tags:
# - 9.66_Runner
# 部署与build使用不同runner使用下面配置,注释deploy1_job
deploy2_job:
stage: deploy
variables:
GIT_STRATEGY: none
# cache: {}
dependencies:
- build_job
script:
- chcp 65001
- xcopy dist E:\web\autodeploy\ /C/Q/E/Y
only:
- master
tags:
- 9.95_Runner
注意:其中对 build_job 的内容做了修改
与 version.sh 同级,新建 build.sh 文件,添加如下内容
1
2
3
4
npm install --registry https://registry.npmjs.org/
npm install --registry https://registry.npm.taobao.org/
npm install --registry http://registry.npm.rdapp.com/
npm run build
这里 install 的第一条和第二条选择一个即可,建议使用 taobao 镜像
注意:只有在 taobao 镜像也下载不下来库时,就需要值用 npmjs 的镜像
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+gitlab-ci自动化部署 - AIGISSS gitlab-ci自动化部署
本文最后更新于:1 分钟前
前言:
此文章是公司同事培训时准备的资料
这里先为大家展示下 gitlab 与 gitlab-ci 的工作流程图,如下

环境准备
打包部署在同一虚拟机
1,虚拟机需要安装 gitlab-runner 服务;
2,安装 node 及 git 环境(如果 node 依赖有用到私仓,需要添加切换私仓镜像)
打包部署在不同虚拟机
1,打包虚拟机安装内容同 1.1;
2,部署虚拟机仅需安装 gitlab-runner 服务即可
gitlab-runner 安装步骤
1,在服务器 C 盘或其他盘符新建一个 GitlabRunner 的文件夹,将文档根目录下的gitlab-runner-windows-amd64.exe
程序拷贝到其中
2,在 GitlabRunner 文件夹目录下 shift+右键,“在此处打开命令窗口”
3,输入 gitlab-runner-windows-amd64.exe register
根据提示填写内容(如下图)

其中 token 是通过管理员登陆 gitlab 后,在 Runners 中复制过来,如下图

注意:tags 的配置可以配置多个,命令行中可以按照 description 的格式来写,后面 yml 文件中会用到
4,输入 gitlab-runner-windows-amd64.exe install
没有报错后再输入 gitlab-runner-windows-amd64.exe start
5,打开任务管理器查看 gitlab-runner 是否正在运行,如下图

6,最终自己登陆上 gitlab 后可以在 看到刚添加上的 gitlab-runner 的状态,如下图状态即为安装成功

部署流程
部署服务器上配置开启 web-server
常用 web-server 有 IIS 和 nginx,配置好后,开启 web 服务后,将存放 web 页面的文件路径记录好,如下
E:\web\autodeploy\
后面 yml 配置文件要用到
gitlab 建立仓储或者使用已有的仓储
这里已 vue 项目为例
1,本地打开项目代码,新建一个名为 .gitlab-ci.yml
文件在项目根目录(与 package.json 同级)
2,.gitlab-ci.yml
中添加配置,详见三
3,git push origin master
push 完成后,在 gitlab 仓储的 CI/CD 中的 Pipeline 中就可以看到部署进度(部署的时长与 npm install 的速度相关)所以建议减少高频 push 代码来触发自动化部署或者通过指定分支来做(在.gitlab-ci.yml 中解释如何操作)
4,push 到远端后,就可以在仓储 CI/CD 的 Pipelines 中看到这条流水线(如下图状态)

5,也可以点开上图红框按钮,进入到当前 job 中查看部署日志状态(如下图),完成后日志结尾会提示 Job succeeded

6,完成后,如下图状态,即为部署成功,这是就可以打开自己在 web-server 上配置的 ip 或者域名来访问网站(此处样例链接 http://autodeploy.rdapp/ )

.gitlab-ci.yml 文件配置解释
.gitlab-ci.yml 每步配置的解释,见下图:

.gitlab-ci.yml 源码,可直接复制,针对项目部署情况修改使用
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
stages:
- build
- deploy
# cache:
# key: "$CI_BUILD_REF_NAME"
# paths:
# - node_modules/
build_job:
stage: build
cache:
key: "$CI_BUILD_REF_NAME"
paths:
- node_modules/
script:
- chcp 65001
- cmd /c version.sh
- cmd /c npm install
- cmd /c npm run build
artifacts:
name: "%CI_COMMIT_REF_NAME%-%CI_COMMIT_SHORT_SHA%"
paths:
- dist/
tags:
- 9.66_Runner
# 部署与build使用相同runner使用下面配置,注释deploy2_job
# deploy1_job:
# stage: deploy
# variables:
# GIT_STRATEGY: none
# cache: {}
# dependencies: []
# script:
# - chcp 65001
# - xcopy dist C:\test\ /C/Q/E/Y
# 拷贝当前目录下所有文件夹及内容
# - xcopy . E:\test /C/Q/E/Y
# only:
# - master
# tags:
# - 9.66_Runner
# 部署与build使用不同runner使用下面配置,注释deploy1_job
deploy2_job:
stage: deploy
variables:
GIT_STRATEGY: none
# cache: {}
dependencies:
- build_job
script:
- chcp 65001
- xcopy dist E:\web\autodeploy\ /C/Q/E/Y
# only:
# - master
tags:
- 9.95_Runner
version.sh 文件配置解释
在第三部分中的.gitlab-ci.yml 代码中 build_job 的 script 中有这么一行命令 - cmd /c version.sh
这里通过执行 version.sh 这么一个 bash 命令文件来生成方便开发和测试的一种辅助功能,具体看下图

version.sh 代码如下
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
git rev-list HEAD | sort > config.git-hash
LOCALVER=`wc -l config.git-hash | awk '{print $1}'`
if [ $LOCALVER \> 1 ] ; then
REVISE=`git rev-list origin/master | sort | join config.git-hash - | wc -l | awk '{print $1}'`
VERSION=`git tag`
echo "$VERSION"
if [ $REVISE != $LOCALVER ] ; then
REVISE="$REVISE+$(($LOCALVER-$REVISE))"
fi
if git status | grep -q "modified:" ; then
REVISE="${REVISE}"
fi
if [ ! -n "$VERSION" ]; then
VERSION="v1.0.0"
else
VERSION=`git describe --tags $(git rev-list --tags --max-count=1)`
fi
GIT_HASH=`git rev-list HEAD -n 1`
GIT_SHORT_HASH=`git rev-list HEAD -n 1 | cut -c 1-8`
TIME=`git log --pretty=format:"%cd" $GIT_SHORT_HASH -1`
echo "$TIME"
else
GIT_VERSION=
REVISE="x"SHORT_
fi
rm -f config.git-hash
cat version.template | sed "s/\$FULL_HASH/$GIT_HASH/g" | sed "s/\$FULL_REVISE/$REVISE/g" | sed "s/\$FULL_VERSION/$VERSION/g" | sed "s/\$FULL_TIME/$TIME/g" > public/$REVISE
cat version.json.template | sed "s/\$FULL_HASH/$GIT_SHORT_HASH/g" | sed "s/\$FULL_REVISE/$REVISE/g" | sed "s/\$FULL_VERSION/$VERSION/g" | sed "s/\$FULL_TIME/$TIME/g" > public/version.json
echo "$VERSION.$REVISE"
echo "Generated version info : $REVISE"
version.template 代码如下:
1
2
3
4
Version $FULL_VERSION
Revise $FULL_REVISE
Git_hash $FULL_HASH
Git_time $FULL_TIME
version.json.template 代码如下:
1
2
3
4
5
6
{
"Tag_version": "$FULL_VERSION",
"Commit_count": "$FULL_REVISE",
"Current_commit_ID": "$FULL_HASH",
"Current_commit_time": "$FULL_TIME"
}
补充
因 build 环节,可能会出现不同项目有不同的 npm 仓,有的包需要用到私仓或者 npm 公仓;这里做以下补充
第四部分的介绍属于扩展,根据项目,按需加
下面介绍需要使用多不同的 npm 镜像时如何处理:
修改.gitlab-ci.yml 文件,如下:
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
stages:
- build
- deploy
build_job:
stage: build
script:
- chcp 65001
- cmd /c version.sh
- cmd /c build.sh
artifacts:
name: "%CI_COMMIT_REF_NAME%-%CI_COMMIT_SHORT_SHA%"
paths:
- dist/
tags:
- 9.101_Runner
# 部署与build使用相同runner使用下面配置,注释deploy2_job
# deploy1_job:
# stage: deploy
# variables:
# GIT_STRATEGY: none
# cache: {}
# dependencies: []
# script:
# - chcp 65001
# - xcopy dist C:\test\ /C/Q/E/Y
# only:
# - master
# tags:
# - 9.66_Runner
# 部署与build使用不同runner使用下面配置,注释deploy1_job
deploy2_job:
stage: deploy
variables:
GIT_STRATEGY: none
# cache: {}
dependencies:
- build_job
script:
- chcp 65001
- xcopy dist E:\web\autodeploy\ /C/Q/E/Y
only:
- master
tags:
- 9.95_Runner
注意:其中对 build_job 的内容做了修改
与 version.sh 同级,新建 build.sh 文件,添加如下内容
1
2
3
4
npm install --registry https://registry.npmjs.org/
npm install --registry https://registry.npm.taobao.org/
npm install --registry http://registry.npm.rdapp.com/
npm run build
这里 install 的第一条和第二条选择一个即可,建议使用 taobao 镜像
注意:只有在 taobao 镜像也下载不下来库时,就需要值用 npmjs 的镜像
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c1.html b/posts/9604e2c1.html
index 9b2bb30b..6eedd6f7 100644
--- a/posts/9604e2c1.html
+++ b/posts/9604e2c1.html
@@ -1 +1 @@
-常见静态网站托管平台使用及多节点部署方案 - AIGISSS 常见静态网站托管平台使用及多节点部署方案
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:Vince
原文地址:https://i.vince.pub/posts/2fc062cb/
前言
对于 Hexo 来说,我们使用它来部署博客是因为无后端运维和高速渲染页面等优点。选择一个合适的托管平台对于博客来说十分重要,可以免费使用且稳定高速的平台是不存在的,我们总是需要做出妥协。我使用了 Github Pages、Coding Pages、Gitee Pages、Netlify 和 Vercel 来部署博客,以下为我的使用报告。
常见托管平台

Github Pages
免费扩展性强无限制性
使用体验:可以与仓库无缝对接,高效部署,但是没用设置国内节点,在国内访问速度较慢,作为一个海外节点还是非常不错的。相对而言,使用 jsdelivr 来加速网站相关文件可以满足基本使用。查看 Github Status,Pages 服务会出现偶尔挂掉的情况,但多数仓库文档、演示等都选择了 Github Pages 服务。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。网站内容与仓库保存一致,自动推送。通过使用 Github Actions 具有较强扩展性。
Netlify
免费扩展性强无限制性
使用体验:Netlify 的节点设置在海外,但 Netlify 的服务速度尚可,国内部分地区可以到达高速服务。在使用 CDN 的情况下,把网站部署在 Netlify 是可以比较好的选择。Vuejs 和 Hexo 的官网都部署在 Netlify 上,其稳定性可想而知。Netlify 虽然拥有付费功能,但是基本上我们需要使用到的服务都免费。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。Netlify 支持 Github 或者 Gitlab 等账号登录,如果仓库已经是静态网站文件,每次 Push 到仓库 Netlify 都会自动部署。支持 Build Command,源文件也可以通过提供的环境自动编译或渲染,类似于一款 CI,与 Github Pages 功能相近。
Vercel
免费扩展性强无限制性
使用体验:Vercel 的体验情况总体和 Netlify 相近,节点设置在海外,访问速度尚可。前身是 now.sh,作为一个高质量的静态托管平台,Vercel的使用体验非常好,是一个可选的优秀平台。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。支持 Github 或者 Gitlab 等账号登录,如果仓库已经是静态网站文件,每次 Push 到仓库都会自动部署。Vercel 打出了 free forever 的口号,也就是说在非商用的情况下,个人可以永久免费使用。支持设置环境并执行相关命令,自动部署不在话下。
Coding
免费一般扩展性限制性
使用体验:Coding 是腾讯系的一个国内托管平台,对于人数较少的团体实行免费制度。服务器节点部署在国内,在国内使用访问速度较快。也是国内开放程度比较高的一个代码托管平台了,静态网站功能 Coding 最近改版了一下,相对于之前来说更稳定了一些。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。基于 Kubernetes 的持续部署,可以人我们体验到与 DevOps 体系紧密结合的持续部署能力。持续中提供静态网站托管,但是静态网站托管需要实名和绑定手机号。
Gitee
免费(国内限制)扩展性较低限制性强
使用体验:Gitee 是一个国内托管平台,对比 coding 来说较为封闭。静态托管功能上拥有较大限制,且无法自动部署,功能残缺。
使用及扩展:提供二级域名,非付费版不支持自动部署、域名绑定及免费 SSL 证书。如果强制使用 https,可能会造成样式文件失效等问题。
TCB
付费扩展性高一般限制性
使用体验:TCB(Tencent CloudBase)采用 serverless 架构,提供静态托管服务。我的主站就是使用 TCB,相对而言因为付费了,所以效果较好,在全国各地有 CDN 节点,目前使用是因为腾讯的赞助计划,如果赞助计划失效了,价格过高可能会考虑切换平台。空间较大,流量较多,已经充当 CDN 使用了。
使用及扩展:提供二级域名,支持自动部署及 免费SSL 证书,但是 SSL 证书申请可能需要备案。扩展性较强,可以使用 CLI 工具或者 Tencent CloudBase Github Action 来部署。
多节点部署方案

几个仓库
Hexo 源码仓库
从图中可以看到使用了 Blog-Source
这个仓库为 Hexo 源码仓库,这个仓库有一个使用了两个 Github Actions,一个用来渲染博客文件并推送到 TCB 静态托管平台,一个用来渲染博客文件推送到各个 Git 仓库,理论上一个 Action 也可以完成这些任务,但是便于管理我选择了两个 Action。
推送至各个 Git 仓库
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
name: Deploy to Repo(Github, Coding, Gitee)
on: [push]
jobs:
build:
runs-on: ubuntu-latest
env:
hTZ: Asia/Shanghai
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: master
- name: Update Submodule
run: |
git submodule init
git submodule update --remote
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: "10.x"
- name: Hexo Generate
run: |
rm -f .yarnclean
yarn --frozen-lockfile --ignore-engines --ignore-optional --non-interactive --silent --ignore-scripts --production=false
rm -rf ./public
yarn run hexo clean
yarn run hexo generate
- name: Hexo Deploy
env:
SSH_PRIVATE: ${{ secrets.SSH_PRIVATE }}
GIT_NAME: vinceying
GIT_EMAIL: admin@vicne.pub
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan e.coding.net >> ~/.ssh/known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-keyscan gitee.com >> ~/.ssh/known_hosts
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
yarn run hexo deploy
推送至 TCB
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
name: Deploy to Tencent CloudBase
on: push
jobs:
build:
runs-on: ubuntu-latest
env:
TZ: Asia/Shanghai
name: Deploy Hexo Souce Repo to Tencent CloudBase
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
# NPM 环境及 Hexo 部署
- name: NPM
run: npm install
- name: Clean
run: ./node_modules/.bin/hexo clean
- name: Generate
run: ./node_modules/.bin/hexo generate
# Deploy static to Tencent CloudBase
- name: Deploy static to Tencent CloudBase
id: deployStatic
uses: TencentCloudBase/cloudbase-action@v1.1.1
with:
secretId: ${{ secrets.SECRET_ID }}
secretKey: ${{ secrets.SECRET_KEY }}
envId: ${{ secrets.ENV_ID }}
staticSrcPath: public
Github 博客页面仓库
这个作为使用 Github Pages 服务的仓库,同时在 Netlify 和 Vercel 的选择为源仓库,在每次推送至本仓库时,Netlify 和 Vercel 都会自动部署新文件。
CDN 文件仓库
这个仓库作为管理和存放一些需要推送到 CDN 的文件,比如 css 文件、图片和视频等,首先是为了便于管理及通过 Github Actions推送 到 TCB,其次是为了使用 Jsdelivr CDN 服务作为备用 CDN。
方案优点
- 高效自动化,利用 Github Actions,每次只要 Push 到
Blog-Souce
和Blog-file
仓库就可以全仓库和全节点同步。 - 便于管理文件,当主 CDN 失效后,直接替换 CDN 地址链接即可完成启用备用 CDN,且备份了文件。
- 多设备管理,当切换设备后,直接在不安装环境的情况下直接 Clone 即可管理博客,但调试方面还是需要安装环境。特别是在 Github 的云端 IDE-Codespace 正式发布后,可以完全通过仓库管理博客。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+常见静态网站托管平台使用及多节点部署方案 - AIGISSS 常见静态网站托管平台使用及多节点部署方案
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:Vince
原文地址:https://i.vince.pub/posts/2fc062cb/
前言
对于 Hexo 来说,我们使用它来部署博客是因为无后端运维和高速渲染页面等优点。选择一个合适的托管平台对于博客来说十分重要,可以免费使用且稳定高速的平台是不存在的,我们总是需要做出妥协。我使用了 Github Pages、Coding Pages、Gitee Pages、Netlify 和 Vercel 来部署博客,以下为我的使用报告。
常见托管平台

Github Pages
免费扩展性强无限制性
使用体验:可以与仓库无缝对接,高效部署,但是没用设置国内节点,在国内访问速度较慢,作为一个海外节点还是非常不错的。相对而言,使用 jsdelivr 来加速网站相关文件可以满足基本使用。查看 Github Status,Pages 服务会出现偶尔挂掉的情况,但多数仓库文档、演示等都选择了 Github Pages 服务。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。网站内容与仓库保存一致,自动推送。通过使用 Github Actions 具有较强扩展性。
Netlify
免费扩展性强无限制性
使用体验:Netlify 的节点设置在海外,但 Netlify 的服务速度尚可,国内部分地区可以到达高速服务。在使用 CDN 的情况下,把网站部署在 Netlify 是可以比较好的选择。Vuejs 和 Hexo 的官网都部署在 Netlify 上,其稳定性可想而知。Netlify 虽然拥有付费功能,但是基本上我们需要使用到的服务都免费。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。Netlify 支持 Github 或者 Gitlab 等账号登录,如果仓库已经是静态网站文件,每次 Push 到仓库 Netlify 都会自动部署。支持 Build Command,源文件也可以通过提供的环境自动编译或渲染,类似于一款 CI,与 Github Pages 功能相近。
Vercel
免费扩展性强无限制性
使用体验:Vercel 的体验情况总体和 Netlify 相近,节点设置在海外,访问速度尚可。前身是 now.sh,作为一个高质量的静态托管平台,Vercel的使用体验非常好,是一个可选的优秀平台。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。支持 Github 或者 Gitlab 等账号登录,如果仓库已经是静态网站文件,每次 Push 到仓库都会自动部署。Vercel 打出了 free forever 的口号,也就是说在非商用的情况下,个人可以永久免费使用。支持设置环境并执行相关命令,自动部署不在话下。
Coding
免费一般扩展性限制性
使用体验:Coding 是腾讯系的一个国内托管平台,对于人数较少的团体实行免费制度。服务器节点部署在国内,在国内使用访问速度较快。也是国内开放程度比较高的一个代码托管平台了,静态网站功能 Coding 最近改版了一下,相对于之前来说更稳定了一些。
使用及扩展:提供二级域名,支持域名绑定及免费 SSL 证书。基于 Kubernetes 的持续部署,可以人我们体验到与 DevOps 体系紧密结合的持续部署能力。持续中提供静态网站托管,但是静态网站托管需要实名和绑定手机号。
Gitee
免费(国内限制)扩展性较低限制性强
使用体验:Gitee 是一个国内托管平台,对比 coding 来说较为封闭。静态托管功能上拥有较大限制,且无法自动部署,功能残缺。
使用及扩展:提供二级域名,非付费版不支持自动部署、域名绑定及免费 SSL 证书。如果强制使用 https,可能会造成样式文件失效等问题。
TCB
付费扩展性高一般限制性
使用体验:TCB(Tencent CloudBase)采用 serverless 架构,提供静态托管服务。我的主站就是使用 TCB,相对而言因为付费了,所以效果较好,在全国各地有 CDN 节点,目前使用是因为腾讯的赞助计划,如果赞助计划失效了,价格过高可能会考虑切换平台。空间较大,流量较多,已经充当 CDN 使用了。
使用及扩展:提供二级域名,支持自动部署及 免费SSL 证书,但是 SSL 证书申请可能需要备案。扩展性较强,可以使用 CLI 工具或者 Tencent CloudBase Github Action 来部署。
多节点部署方案

几个仓库
Hexo 源码仓库
从图中可以看到使用了 Blog-Source
这个仓库为 Hexo 源码仓库,这个仓库有一个使用了两个 Github Actions,一个用来渲染博客文件并推送到 TCB 静态托管平台,一个用来渲染博客文件推送到各个 Git 仓库,理论上一个 Action 也可以完成这些任务,但是便于管理我选择了两个 Action。
推送至各个 Git 仓库
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
name: Deploy to Repo(Github, Coding, Gitee)
on: [push]
jobs:
build:
runs-on: ubuntu-latest
env:
hTZ: Asia/Shanghai
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: master
- name: Update Submodule
run: |
git submodule init
git submodule update --remote
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: "10.x"
- name: Hexo Generate
run: |
rm -f .yarnclean
yarn --frozen-lockfile --ignore-engines --ignore-optional --non-interactive --silent --ignore-scripts --production=false
rm -rf ./public
yarn run hexo clean
yarn run hexo generate
- name: Hexo Deploy
env:
SSH_PRIVATE: ${{ secrets.SSH_PRIVATE }}
GIT_NAME: vinceying
GIT_EMAIL: admin@vicne.pub
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan e.coding.net >> ~/.ssh/known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-keyscan gitee.com >> ~/.ssh/known_hosts
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
yarn run hexo deploy
推送至 TCB
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
name: Deploy to Tencent CloudBase
on: push
jobs:
build:
runs-on: ubuntu-latest
env:
TZ: Asia/Shanghai
name: Deploy Hexo Souce Repo to Tencent CloudBase
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
# NPM 环境及 Hexo 部署
- name: NPM
run: npm install
- name: Clean
run: ./node_modules/.bin/hexo clean
- name: Generate
run: ./node_modules/.bin/hexo generate
# Deploy static to Tencent CloudBase
- name: Deploy static to Tencent CloudBase
id: deployStatic
uses: TencentCloudBase/cloudbase-action@v1.1.1
with:
secretId: ${{ secrets.SECRET_ID }}
secretKey: ${{ secrets.SECRET_KEY }}
envId: ${{ secrets.ENV_ID }}
staticSrcPath: public
Github 博客页面仓库
这个作为使用 Github Pages 服务的仓库,同时在 Netlify 和 Vercel 的选择为源仓库,在每次推送至本仓库时,Netlify 和 Vercel 都会自动部署新文件。
CDN 文件仓库
这个仓库作为管理和存放一些需要推送到 CDN 的文件,比如 css 文件、图片和视频等,首先是为了便于管理及通过 Github Actions推送 到 TCB,其次是为了使用 Jsdelivr CDN 服务作为备用 CDN。
方案优点
- 高效自动化,利用 Github Actions,每次只要 Push 到
Blog-Souce
和Blog-file
仓库就可以全仓库和全节点同步。 - 便于管理文件,当主 CDN 失效后,直接替换 CDN 地址链接即可完成启用备用 CDN,且备份了文件。
- 多设备管理,当切换设备后,直接在不安装环境的情况下直接 Clone 即可管理博客,但调试方面还是需要安装环境。特别是在 Github 的云端 IDE-Codespace 正式发布后,可以完全通过仓库管理博客。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c2.html b/posts/9604e2c2.html
index e4b43ebb..79eb9b10 100644
--- a/posts/9604e2c2.html
+++ b/posts/9604e2c2.html
@@ -1,4 +1,4 @@
-Hexo 云服务备份与使用 Jupyter - AIGISSS Hexo 云服务备份与使用 Jupyter
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:pxxyyz
原文地址:https://pxxyyz.com/posts/32990/、https://pxxyyz.com/posts/60533/
前言
记录hexo博客遇到的问题:
- 免密git
- 自动备份
- 云服务器开启
Jupyter Notebook
- 在博客的菜单访问
Jupyter
(使用Nginx
重定向实现url访问端口) - 公式渲染引擎
云服务器备份
参考「Hexo博客部署到腾讯云服务器」Hexo 云服务备份与使用 Jupyter - AIGISSS Hexo 云服务备份与使用 Jupyter
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:pxxyyz
原文地址:https://pxxyyz.com/posts/32990/、https://pxxyyz.com/posts/60533/
前言
记录hexo博客遇到的问题:
- 免密git
- 自动备份
- 云服务器开启
Jupyter Notebook
- 在博客的菜单访问
Jupyter
(使用Nginx
重定向实现url访问端口) - 公式渲染引擎
云服务器备份
参考「Hexo博客部署到腾讯云服务器」[1]后遇到了两个问题:
每次在本地部署博客时都要重复输入密码
1
2
3
4
deploy:
type: git
repo: root@***(服务器ip,内网外网都行):/home/git/blog.git #仓库地址
branch: master #分支
备份时hexo自带的backup无效
1
2
3
4
backup:
type: git
repo: root@***(服务器ip,内网外网都行):/home/git/backup.git #仓库地址
branch: master #分支
免密git
在这一部分参照了「使用Git+Hooks实现Hexo站点自动部署到CentOS服务器上」[2]的配置SSH免密登陆步骤。
1
2
$ ssh-copy-id -i ~/.ssh/id_rsa.pub your_user_name@HostIP //添加公钥
$ ssh your_user_name@HostIP //验证是否添加成功
因为部署时用的root上传,因此这里的your_user_name
设置git
和root
两个。添加成功后ssh -v git@HostIP
和ssh -v root@HostIP
显示Welcome to XXX !
自动备份
这里我按照「deploy的流程在服务器设置了自动化备份」[1],主要思路是在服务器设置一个独立的文件夹backup
,再用类似deploy的钩子blog.git
,构造一个备份的钩子deploy.git
将博客的备份文件上传。
获取root
权限
1
$ su root
建立git
仓库
1
2
$ cd /home/backup
$ git init --bare backup.git
修改backup.git
权限
1
$ chown git:git -R backup.git
在 /home/hexo/backup.git
下,有一个自动生成的 hooks
文件夹,我们创建一个新的 git
钩子 post-receive
,用于自动部署。
1
$ vim backup.git/hooks/post-receive
按 i
键进入文件的编辑模式,在该文件中添加两行代码(将下边的代码粘贴进去),指定 Git 的工作树(源代码)和 Git 目录
1
2
3
4
5
#!/bin/bash
git --work-tree=/home/backup --git-dir=/home/git/backup.git remote add origin
git --work-tree=/home/backup --git-dir=/home/git/backup.git checkout -f
# git --work-tree=/home/backup --git-dir=/home/git/backup.git checkout -b master # 创建切换分支
git --work-tree=/home/backup --git-dir=/home/git/backup.git push origin master # 提交代码至分支
按 Esc
键退出编辑模式,输入:wq
保存退出。(先输入:
,然后输入wq
回车)
修改文件权限,使得其可执行。
1
$ chmod +x /home/git/backup.git/hooks/post-receive
博客根目录_config
下增加(因为服务器没有分支,默认是master
,使用backup
钩子)
1
2
3
4
deploy:
type: git
repo: root@***(服务器ip,内网外网都行):/home/git/backup.git #仓库地址
branch: master #分支
备份hexo backup
(使用 Hexo-Git-Backup
插件)
1
2
3
$ hexo clean
$ hexo g
$ hexo b
这个地方走了不少弯路,因为backup阶段每次都提示错误:
1
2
fatal: 'xxx' does not appear to be a git repository
fatal: Could not read from remote repository.
我通过搜索fatal-does-not-appear-to-be-a-git-repository
找到解决思路,用Git命令
自动备份。详细参见「HEXO博客实现自动备份」使用 Rainbow 展示随机的英语句子 - AIGISSS 使用 Rainbow 展示随机的英语句子
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:吃白饭
原文地址:https://eatrice.top/post/Rainbow/
访问 Rainbow 吧!
介绍
Rainbow - 一朵彩虹是 EatRiceTeam
建立的一个旨在收集优美英语句子的一套网站。演示网站地址为:https://rainbow.eatrice.top/
我们希望能够与大家分享我们在日常的学习生活中遇到的优美的英语句子,希望它能像彩虹一样,美丽天空,温暖人心。
其由C#
开发,基于ASP.NET Core 2.2
框架。包括Web API
提供导出JSON
的数据接口,和基于MVC
的动态展示网站。
关于Rainbow
Rainbow 收集的英语句子的要求为:
- 读起来感觉很优美的文章句子段落或诗歌节选;
- 含义特别丰富且引人深思的鸡汤或哲学句子;
- 句子奇怪,但意义完整且显得很有个性的电影台词;
- 你特别喜欢,且引起你感情上共鸣的英语歌词。
Rainbow 创建的初衷是替代我们的个人网站目前正在使用的 一言 ,我们希望自己能够自己定义一句话的意思和表现形式。目前句子库不是特别丰富,收集的资源有限,所以欢迎大家投稿,并发表自己的看法。
给Rainbow投稿
我们希望找到小伙伴们和我们一起充实我们的句子库,希望大家能够将自己珍藏的句子分享给我们:
投稿要求:
- 提供完整的句子
- 提供句子的作者
- 提供句子的来源,如书名、文章名、电影名、歌曲名等。
投稿方式:
在本页下方留言
好友QQ或微信直接发送
使用方法
数据接口
目前语句库饱含了三种类型的语句:reading、movies、songs
需要从语句库中随机获得语句的Json
格式的接口:https://api.eatrice.top/
需要按照三个单独分类请求语句的接口:
https://api.eatrice.top/reading/
https://api.eatrice.top/movies/
https://api.eatrice.top/songs/
获取所有的句子接口:https://api.eatrice.top/GetAll/
需要根据语句ID请求语句的接口:https://api.eatrice.top/?ID=10001
ID编号从10001开始增加,若该ID不存在则随机返回语句,同https://api.eatrice.top/
返回的数据格式如下:
1
2
3
4
5
6
{
"Content": "Because I am your mom,It counts the most because I know you the most.",
"Author": "Stephen Chbosky",
"Source": "Wonder",
"ID": "10009"
}
其中,Content
为句子内容
Author
为句子作者
Source
为句子来源
ID
为句子ID展示网站
展示网站为 Rainbow 提供展示界面。和说明文档。
Rainbow的展示网站为:https://rainbow.eatrice.top/
欢迎大家访问和提供意见😊😊😊。

安装
在需要添加 rainbow
的地方添加一个占位符:
1
<p><a id="rainbow" href=''>🌈 获取中...</a></p>
在申明div之后,搭配数据请求脚本
1
2
3
4
5
6
7
8
9
10
<script>
fetch('https://api.eatrice.top')
.then(response => response.json())
.then(data => {
var rainbow = document.getElementById('rainbow');
rainbow.innerHTML = data.Content;
rainbow.href = "https://rainbow.eatrice.top/?ID=" + data.ID;
})
.catch(console.error)
</script>
就能在网站上看到你的 rainbow 啦!
开源
项目已在github
上开源
贡献者
EatRice-https://eatrice.top
上屋顶看北斗七星-https://ruru.eatrice.top
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+使用 Rainbow 展示随机的英语句子 - AIGISSS 使用 Rainbow 展示随机的英语句子
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:吃白饭
原文地址:https://eatrice.top/post/Rainbow/
访问 Rainbow 吧!
介绍
Rainbow - 一朵彩虹是 EatRiceTeam
建立的一个旨在收集优美英语句子的一套网站。演示网站地址为:https://rainbow.eatrice.top/
我们希望能够与大家分享我们在日常的学习生活中遇到的优美的英语句子,希望它能像彩虹一样,美丽天空,温暖人心。
其由C#
开发,基于ASP.NET Core 2.2
框架。包括Web API
提供导出JSON
的数据接口,和基于MVC
的动态展示网站。
关于Rainbow
Rainbow 收集的英语句子的要求为:
- 读起来感觉很优美的文章句子段落或诗歌节选;
- 含义特别丰富且引人深思的鸡汤或哲学句子;
- 句子奇怪,但意义完整且显得很有个性的电影台词;
- 你特别喜欢,且引起你感情上共鸣的英语歌词。
Rainbow 创建的初衷是替代我们的个人网站目前正在使用的 一言 ,我们希望自己能够自己定义一句话的意思和表现形式。目前句子库不是特别丰富,收集的资源有限,所以欢迎大家投稿,并发表自己的看法。
给Rainbow投稿
我们希望找到小伙伴们和我们一起充实我们的句子库,希望大家能够将自己珍藏的句子分享给我们:
投稿要求:
- 提供完整的句子
- 提供句子的作者
- 提供句子的来源,如书名、文章名、电影名、歌曲名等。
投稿方式:
在本页下方留言
好友QQ或微信直接发送
使用方法
数据接口
目前语句库饱含了三种类型的语句:reading、movies、songs
需要从语句库中随机获得语句的Json
格式的接口:https://api.eatrice.top/
需要按照三个单独分类请求语句的接口:
https://api.eatrice.top/reading/
https://api.eatrice.top/movies/
https://api.eatrice.top/songs/
获取所有的句子接口:https://api.eatrice.top/GetAll/
需要根据语句ID请求语句的接口:https://api.eatrice.top/?ID=10001
ID编号从10001开始增加,若该ID不存在则随机返回语句,同https://api.eatrice.top/
返回的数据格式如下:
1
2
3
4
5
6
{
"Content": "Because I am your mom,It counts the most because I know you the most.",
"Author": "Stephen Chbosky",
"Source": "Wonder",
"ID": "10009"
}
其中,Content
为句子内容
Author
为句子作者
Source
为句子来源
ID
为句子ID展示网站
展示网站为 Rainbow 提供展示界面。和说明文档。
Rainbow的展示网站为:https://rainbow.eatrice.top/
欢迎大家访问和提供意见😊😊😊。

安装
在需要添加 rainbow
的地方添加一个占位符:
1
<p><a id="rainbow" href=''>🌈 获取中...</a></p>
在申明div之后,搭配数据请求脚本
1
2
3
4
5
6
7
8
9
10
<script>
fetch('https://api.eatrice.top')
.then(response => response.json())
.then(data => {
var rainbow = document.getElementById('rainbow');
rainbow.innerHTML = data.Content;
rainbow.href = "https://rainbow.eatrice.top/?ID=" + data.ID;
})
.catch(console.error)
</script>
就能在网站上看到你的 rainbow 啦!
开源
项目已在github
上开源
贡献者
EatRice-https://eatrice.top
上屋顶看北斗七星-https://ruru.eatrice.top
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c4.html b/posts/9604e2c4.html
index f454a93b..c897ce0f 100644
--- a/posts/9604e2c4.html
+++ b/posts/9604e2c4.html
@@ -1,3 +1,3 @@
-给博客文章嵌入 PPT 演示 - AIGISSS 给博客文章嵌入 PPT 演示
本文最后更新于:6 个月前
效果
可以通过鼠标和键盘控制
- 页面: ↑/↓/←/→ Space Home End(空格,home键,end键)
- 全屏: F
- Overview: -/+
- 演讲者笔记: N
- 网格背景: Enter
nodeppt
首先可以看看官网给的demo,非常的炫酷。
安装nodeppt
1
$ npm install -g nodeppt
使用nodeppt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# new:使用线上模板创建一个新的 md 文件
# create a new slide with an official template
$ nodeppt new slide.md
# 使用模板
$ nodeppt new username/repo xxx.md
# create a new slide straight from a github template
$ nodeppt new slide.md -t username/repo
# serve:启动一个 md 文件的 webpack dev server
# start local sever show slide
$ nodeppt serve slide.md
# start local sever show slide with port
$ nodeppt serve slide.md -p port
# build:编译产出一个 md 文件
# to build a slide
$ nodeppt build slide.md
生成的网页可以使用键盘操作(类似PPT操作)
- Page: ↑/↓/←/→ Space Home End
- Fullscreen: F
- Overview: -/+
- Speaker Note: N
- Grid Background: Enter
- nodeppt 有演讲者模式,在页面 url 后面增加
?mode=speaker
既可以打开演讲者模式,双屏同步
端口port的好处是可以照着官网的demo文件学习和修改,保证多个slide.md在浏览器查看时不会冲突,默认的链接是http://192.168.0.105:8080/。
产生pdf:直接在浏览器上command+P/ctrl+P
产生html:
之前版本通过nodeppt generate ./ppts/demo.md -a
,见Github nodePPT v1.2.0
当前版本产生html利用built指令 ,例如nodeppt build slide.md
,产生的html在默认文件夹dist
中,包含CSS、IMG、JS三个文件夹和demo.html。
在nodeppt仓库的Issue给博客文章嵌入 PPT 演示 - AIGISSS 给博客文章嵌入 PPT 演示
本文最后更新于:6 个月前
效果
可以通过鼠标和键盘控制
- 页面: ↑/↓/←/→ Space Home End(空格,home键,end键)
- 全屏: F
- Overview: -/+
- 演讲者笔记: N
- 网格背景: Enter
nodeppt
首先可以看看官网给的demo,非常的炫酷。
安装nodeppt
1
$ npm install -g nodeppt
使用nodeppt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# new:使用线上模板创建一个新的 md 文件
# create a new slide with an official template
$ nodeppt new slide.md
# 使用模板
$ nodeppt new username/repo xxx.md
# create a new slide straight from a github template
$ nodeppt new slide.md -t username/repo
# serve:启动一个 md 文件的 webpack dev server
# start local sever show slide
$ nodeppt serve slide.md
# start local sever show slide with port
$ nodeppt serve slide.md -p port
# build:编译产出一个 md 文件
# to build a slide
$ nodeppt build slide.md
生成的网页可以使用键盘操作(类似PPT操作)
- Page: ↑/↓/←/→ Space Home End
- Fullscreen: F
- Overview: -/+
- Speaker Note: N
- Grid Background: Enter
- nodeppt 有演讲者模式,在页面 url 后面增加
?mode=speaker
既可以打开演讲者模式,双屏同步
端口port的好处是可以照着官网的demo文件学习和修改,保证多个slide.md在浏览器查看时不会冲突,默认的链接是http://192.168.0.105:8080/。
产生pdf:直接在浏览器上command+P/ctrl+P
产生html:
之前版本通过nodeppt generate ./ppts/demo.md -a
,见Github nodePPT v1.2.0
当前版本产生html利用built指令 ,例如nodeppt build slide.md
,产生的html在默认文件夹dist
中,包含CSS、IMG、JS三个文件夹和demo.html。
在nodeppt仓库的Issue[1]上找到一个小哥做的爬虫程序,亲测有效。会生成一个html文件,虽然文件会大一点。不过用index.md文件实验,发现(某些)图片响应时间过长导致失败,不过自己写的markdown基本无压力转html,给小哥点大大的赞👍而且小哥表示:
之前试过直接用build,效果没问题,但build出来会有几个文件,如果通过手机或email分享出去直接播放的话稍显麻烦。
nodeppt入门
- 配置与hexo的post文件头一样,用 yaml 语法设定基本配置
1
2
3
4
5
6
7
8
9
10
title: nodeppt markdown 演示
speaker: 三水清
url: https://github.com/ksky521/nodeppt
js:
- https://www.echartsjs.com/asset/theme/shine.js
prismTheme: solarizedlight
plugins:
- echarts
- mermaid
- katex
- 正文使用
<slide>
对整个 markdown 文件进行拆分,拆成单页的幻灯片内容。 - 图片、样式、布局、icon、动画等设置可以看看仓库的文档和demo文件学习。
- 演讲者模式的批注通过来
:::
语法添加,然后再页面的链接添加?mode=speaker
,按N
开启演讲中模式。
1
2
3
:::note
## Note here
:::
踩坑
CSS样式导入失败
生成的html数学公式的格式全部错误,即使在nodeppt的配置部分引入katex的JS和CSS,导出的文档仍然会出错。
- 通过nodeppt build的html页面
打开生成的html文件可以看到[2]
1
2
3
4
<link rel="stylesheet" href="//cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" />
<link rel="stylesheet" href="//cdn.staticfile.org/prism/1.15.0/themes/prism.min.css" />
<link rel="stylesheet" href="//cdn.staticfile.org/KaTeX/0.10.0-rc.1/katex.min.css" />
<link rel="stylesheet" href="//cdn.staticfile.org/KaTeX/0.5.1/katex.min.css" />
只要把文件中所有//
开头的都替换成https://
,如
1
2
3
4
<link rel=stylesheet href=https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css>
<link rel=stylesheet href=https://cdn.staticfile.org/prism/1.15.0/themes/prism.min.css>
<link rel=stylesheet href=https://cdn.staticfile.org/KaTeX/0.10.0-rc.1/katex.min.css>
<link rel=stylesheet href=https://cdn.staticfile.org/KaTeX/0.5.1/katex.min.css>
这样控制台就不会报错了,数学公式和fa-icon能正常显示了。
- 通过py程序爬的html页面
配合KaTeX官网的使用文档,在生成的html文件<head>
引用katex的JS和CSS。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<!-- KaTeX requires the use of the HTML5 doctype. Without it, KaTeX may not render properly -->
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css" integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq" crossorigin="anonymous">
<!-- The loading of KaTeX is deferred to speed up page rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js" integrity="sha384-y23I5Q6l+B6vatafAwxRu/0oK/79VlbSz7Q9aiSZUvyWYIYsd+qj+o24G5ZU2zJz" crossorigin="anonymous"></script>
<!-- To automatically render math in text elements, include the auto-render extension: -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
</head>
...
</html>
添加后公式都能正确显示了。
在博客添加nodeppt
- 通过py程序爬的html页面
在Hexo博客里想调用或者链接nodeppt生成的html,需要hexo设置skip_render
, 指定不进行渲染的文件或文件夹,例如在source
目录下新建nodeppt
来存放nodeppt生成的html,则需要在根目录下的_config.yml
文件添加
1
2
skip_render:
- nodeppt/*.html
- 通过nodeppt build的html页面
1
2
skip_render:
- nodeppt/**
文件匹配是基于正则匹配的,如果需要忽略全部文件(/*
)、指定类型type文件(/*.type
)、全部文件以及子目录(/**
)以及多个文件需要用(- file/**
)。
对应的文件访问格式是../../nodeppt/file.html
或../../nodeppt/file/demo.html
,本页演示的加载是通过iframe
实现的。
1
<iframe src="../../nodeppt/file.html" width="100%" height="500" name="topFrame" scrolling="yes" noresize="noresize" frameborder="0" id="topFrame"></iframe>
注意:如果这一步不执行的话,debug会发现nodeppt生成的html会被hexo处理,产生错误
1
2
FATAL Something's wrong. Maybe you can find the solution here: https://hexo.io/docs/troubleshooting.html
Nunjucks Error: [Line 9418, Column 3465] expected variable end
至于使用cdn来使用html似乎不行,出来的是html的源码,而不是网页。如果使用cdn的方式能成功就不用这么麻烦的skip_render
。
有一种简单的方法就是用github或者coding等部署nodeppt的html,再iframe的src填对应的网址。如果hexo的skip_render
设置正确,也可通过网址主页下的nodeppt下找到。
- https://pxxyyz.com/nodeppt/%E5%A4%9A%E5%A4%8D%E5%8F%98%E8%BF%91%E6%9C%9F%E8%BF%9B%E5%B1%95/demo.html
- http://uwrfy5.coding-pages.com/
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c5.html b/posts/9604e2c5.html
index 5f4d691f..1f7b2405 100644
--- a/posts/9604e2c5.html
+++ b/posts/9604e2c5.html
@@ -1 +1 @@
-Hexo 暗黑模式 - AIGISSS Hexo 暗黑模式
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:Royce
原文地址:https://royce2003.top/posts/41212.html
大概花了一个晚上搞暗黑模式,之后陆续优化了下
目前博客已经基本上适配完成了
目前是三种方案(优先级递减)
- 媒体查询
- 定时开启
- localStorage/sessionStorage 查询
媒体查询
,判断系统是否处于暗黑模式,支持大部分系统
Win10 需要浏览器开启软件深色模式
Android 同理,需要浏览器支持手机开启夜间模式的时候将自身切换到神色模式,目前 Chrome 支持,Edge 不支持,其他没测
iOS、MacOS 上的 Safari 也支持
定时开启
,在规定时间自动开启,如果在该时间段内取消了暗黑模式,能一直保持
localStorage/sessionStorage 查询
,能一直保持某一个模式的依赖
HTML
在 \themes\fluid\layout\layout.ejs
中找到 <body>
,在其之后加入如下代码
1
2
3
4
5
6
7
8
9
10
11
<div id="dark" onclick="switchDarkMode()"></div>
<script>
var isNight = new Date().getHours() >= 22 || new Date().getHours() < 7; // 指定时间
// 依次判断 系统暗黑模式 指定时间 缓存 dark
if( matchMedia('(prefers-color-scheme: dark)').matches || isNight || localStorage.getItem('dark') === '1') {
if(!(isNight&&localStorage.getItem('noDark') === '1')) {
document.body.classList.add('dark');
}
}
document.getElementById('dark').innerHTML = document.querySelector("body").classList.contains("dark")?"🌙":"🌞";
</script>
注意!一定紧跟在 body
标签之后,否则会出现闪烁
JS
在自定义 JS 中把下面代码加进去,直接加到 </body>
之前也行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//点击事件
function switchDarkMode() {
if ($('body').hasClass('dark')) {
$("#dark").html("🌞");
document.body.classList.remove('dark');
localStorage.setItem('noDark', '1');
localStorage.setItem('dark', '0');
} else {
$("#dark").html("🌙");
document.body.classList.add('dark');
localStorage.setItem('dark', '1');
localStorage.setItem('noDark', '0');
}
}
CSS
在自定义 CSS 中加入代码
可以用 stylus
,能少些写
但是引入时记得后缀还是 .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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/* 切换按钮 */
#dark
cursor pointer
position fixed
right 40px
bottom 98px
width 16px
height 14px
z-index 100
font-size 20px
/*暗黑模式*/
.dark
background-color #282c34
/* 背景遮罩 */
.mask
background-color rgba(0,0,0,.7) !important
/* 主体 */
#board
background-color #282c34
color #a09c9c
img
filter brightness(50%) // 图片亮度
p
.index-info a
color #a09c9c !important
.markdown-body
h1,h2,h3,h4,h5,h6,s,li
color:#a09c9c !important
/* 顶栏 */
.navbar-col-show
.top-nav-collapse
background-color #282c34
.navbar a
color #a09c9c !important
.animated-icon span /* 手机端 */
background-color #a09c9c
/* page-number */
.pagination a:hover
.pagination .current
background-color #6b6b6b73;
/* 打字机 */
#subtitle
.dark.typed-cursor--blink
.scroll-down-arrow
color #dfdfdf
/* back to top */
#scroll-top-button
background-color #282c34
i
color #a09c9c
/* Toc */
.tocbot-list a
color #a09c9c
.tocbot-active-link
footer a:hover
color #1abc9c !important
/* footer */
footer
footer a
color #a09c9c
/* 归档页 */
.list-group-item
color #a09c9c
background-color #282c34
.list-group-item:hover
.tagcloud a:hover
background-color #46484d
/* 友链页 */
.links
.card
background-color #282c34
.card-body:hover
background-color #46484d
.link-title
.link-intro
color #a09c9c
/* note标签 配色有点丑 */
.note-info
background-color #3b5359
border-color #006d80
.note-danger
background-color #783f42
border-color #670009
.note-success
background-color #2a3e2e
border-color #005915
.note-warning
background-color #5b543e
border-color #846500
.note-primary
background-color #455a6f
border-color #004188
localStorage
仔细观察刚刚的 js 代码,在其中用到了 localStorage
相当于一个标记,除非被手动清除,否则将会永久保存。
下面是支持该特性的最低版本

可以在浏览器控制台中查看他们的值

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+Hexo 暗黑模式 - AIGISSS Hexo 暗黑模式
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:Royce
原文地址:https://royce2003.top/posts/41212.html
大概花了一个晚上搞暗黑模式,之后陆续优化了下
目前博客已经基本上适配完成了
目前是三种方案(优先级递减)
- 媒体查询
- 定时开启
- localStorage/sessionStorage 查询
媒体查询
,判断系统是否处于暗黑模式,支持大部分系统
Win10 需要浏览器开启软件深色模式
Android 同理,需要浏览器支持手机开启夜间模式的时候将自身切换到神色模式,目前 Chrome 支持,Edge 不支持,其他没测
iOS、MacOS 上的 Safari 也支持
定时开启
,在规定时间自动开启,如果在该时间段内取消了暗黑模式,能一直保持
localStorage/sessionStorage 查询
,能一直保持某一个模式的依赖
HTML
在 \themes\fluid\layout\layout.ejs
中找到 <body>
,在其之后加入如下代码
1
2
3
4
5
6
7
8
9
10
11
<div id="dark" onclick="switchDarkMode()"></div>
<script>
var isNight = new Date().getHours() >= 22 || new Date().getHours() < 7; // 指定时间
// 依次判断 系统暗黑模式 指定时间 缓存 dark
if( matchMedia('(prefers-color-scheme: dark)').matches || isNight || localStorage.getItem('dark') === '1') {
if(!(isNight&&localStorage.getItem('noDark') === '1')) {
document.body.classList.add('dark');
}
}
document.getElementById('dark').innerHTML = document.querySelector("body").classList.contains("dark")?"🌙":"🌞";
</script>
注意!一定紧跟在 body
标签之后,否则会出现闪烁
JS
在自定义 JS 中把下面代码加进去,直接加到 </body>
之前也行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//点击事件
function switchDarkMode() {
if ($('body').hasClass('dark')) {
$("#dark").html("🌞");
document.body.classList.remove('dark');
localStorage.setItem('noDark', '1');
localStorage.setItem('dark', '0');
} else {
$("#dark").html("🌙");
document.body.classList.add('dark');
localStorage.setItem('dark', '1');
localStorage.setItem('noDark', '0');
}
}
CSS
在自定义 CSS 中加入代码
可以用 stylus
,能少些写
但是引入时记得后缀还是 .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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/* 切换按钮 */
#dark
cursor pointer
position fixed
right 40px
bottom 98px
width 16px
height 14px
z-index 100
font-size 20px
/*暗黑模式*/
.dark
background-color #282c34
/* 背景遮罩 */
.mask
background-color rgba(0,0,0,.7) !important
/* 主体 */
#board
background-color #282c34
color #a09c9c
img
filter brightness(50%) // 图片亮度
p
.index-info a
color #a09c9c !important
.markdown-body
h1,h2,h3,h4,h5,h6,s,li
color:#a09c9c !important
/* 顶栏 */
.navbar-col-show
.top-nav-collapse
background-color #282c34
.navbar a
color #a09c9c !important
.animated-icon span /* 手机端 */
background-color #a09c9c
/* page-number */
.pagination a:hover
.pagination .current
background-color #6b6b6b73;
/* 打字机 */
#subtitle
.dark.typed-cursor--blink
.scroll-down-arrow
color #dfdfdf
/* back to top */
#scroll-top-button
background-color #282c34
i
color #a09c9c
/* Toc */
.tocbot-list a
color #a09c9c
.tocbot-active-link
footer a:hover
color #1abc9c !important
/* footer */
footer
footer a
color #a09c9c
/* 归档页 */
.list-group-item
color #a09c9c
background-color #282c34
.list-group-item:hover
.tagcloud a:hover
background-color #46484d
/* 友链页 */
.links
.card
background-color #282c34
.card-body:hover
background-color #46484d
.link-title
.link-intro
color #a09c9c
/* note标签 配色有点丑 */
.note-info
background-color #3b5359
border-color #006d80
.note-danger
background-color #783f42
border-color #670009
.note-success
background-color #2a3e2e
border-color #005915
.note-warning
background-color #5b543e
border-color #846500
.note-primary
background-color #455a6f
border-color #004188
localStorage
仔细观察刚刚的 js 代码,在其中用到了 localStorage
相当于一个标记,除非被手动清除,否则将会永久保存。
下面是支持该特性的最低版本

可以在浏览器控制台中查看他们的值

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c6.html b/posts/9604e2c6.html
index 3f5ff739..1b2e04a0 100644
--- a/posts/9604e2c6.html
+++ b/posts/9604e2c6.html
@@ -1,4 +1,4 @@
-Hello Fluid - AIGISSS Hello Fluid
本文最后更新于:6 个月前
欢迎体验 Fluid ,这是一款 Material Design 风格的 Hexo 主题,以简约的设计帮助你专注于写作,本篇文章可预览主题的样式及功能。
文字
文章大部分使用的是 github-markdown 样式,并加入了一些 Material 风格。
H3 标题
H4 标题
粗体
斜体
代码
行内代码:$ hexo new post "My New Post"
代码高亮使用的是 highlight.js,支持 185 种语言和 91 种高亮样式:
1
2
3
4
5
6
7
def fib(n):
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
fib(1000)
1
2
3
4
5
6
type Map struct {
mu Mutex
read atomic.Value
dirty map[interface{}]*entry
misses int
}
表格
Header 1 Header 2 Header 3 Key 1 Value 1 Comment 1 Key 2 Value 2 Comment 2 Key 3 Value 3 Comment 3
列表
有序列表
Fluid 相较于其他主题的优势:
- 设计遵循简洁至上,同时具有轻快的体验,和优雅的颜值;
- 提供大量定制化配置项,使每个用户使用该主题都能具有独特的样式;
- 响应式页面,适配手机、平板等设备;
无序列表
Fluid 功能特性:
- 图片懒加载
- 自定义代码高亮方案
- 内置多语言
- 支持多款评论插件
- 支持使用数据文件存放配置
- 自定义静态资源 CDN
- 内置文章搜索
- 页脚备案信息
- 网页访问统计
- 支持 LaTeX 数学公式
- 支持 mermaid 流程图
- 音乐播放器
图片

LaTex
基于 MathJax 引擎:
流程图
基于 mermaid 语法:
sequenceDiagram
+Hello Fluid - AIGISSS Hello Fluid
本文最后更新于:6 个月前
欢迎体验 Fluid ,这是一款 Material Design 风格的 Hexo 主题,以简约的设计帮助你专注于写作,本篇文章可预览主题的样式及功能。
文字
文章大部分使用的是 github-markdown 样式,并加入了一些 Material 风格。
H3 标题
H4 标题
粗体
斜体
代码
行内代码:$ hexo new post "My New Post"
代码高亮使用的是 highlight.js,支持 185 种语言和 91 种高亮样式:
1
2
3
4
5
6
7
def fib(n):
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
fib(1000)
1
2
3
4
5
6
type Map struct {
mu Mutex
read atomic.Value
dirty map[interface{}]*entry
misses int
}
表格
Header 1 Header 2 Header 3 Key 1 Value 1 Comment 1 Key 2 Value 2 Comment 2 Key 3 Value 3 Comment 3
列表
有序列表
Fluid 相较于其他主题的优势:
- 设计遵循简洁至上,同时具有轻快的体验,和优雅的颜值;
- 提供大量定制化配置项,使每个用户使用该主题都能具有独特的样式;
- 响应式页面,适配手机、平板等设备;
无序列表
Fluid 功能特性:
- 图片懒加载
- 自定义代码高亮方案
- 内置多语言
- 支持多款评论插件
- 支持使用数据文件存放配置
- 自定义静态资源 CDN
- 内置文章搜索
- 页脚备案信息
- 网页访问统计
- 支持 LaTeX 数学公式
- 支持 mermaid 流程图
- 音乐播放器
图片

LaTex
基于 MathJax 引擎:
流程图
基于 mermaid 语法:
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
diff --git a/posts/9604e2c7.html b/posts/9604e2c7.html
index 7b1892b4..da2ae49f 100644
--- a/posts/9604e2c7.html
+++ b/posts/9604e2c7.html
@@ -1 +1 @@
-搭配 Fluid 如何优雅的写一篇文章 - AIGISSS 搭配 Fluid 如何优雅的写一篇文章
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:Vince
原文地址:https://i.vince.pub/posts/14677127/
前言
Fluid 是一款很十分优雅的主题,那么写一篇优雅的文章搭配它呢?以下会从几个方面来简述,主要还是做几个推荐。
文章内容
熟悉 Markdown 语法
对于使用 Hexo 的大多数人来说,相信对 Markdown 的语法不会陌生。熟练掌握 Markdown 语法对我们的写作拥有极大的帮助,这里用少用的表格和脚注来举个例子。至于为什么有些公式、流程图无法渲染,是因为 Markdown 追求简洁式写作,默认渲染器不支持复杂渲染。
表格
站点 地址 介绍 Fluid Docs https://hexo.fluid-dev.com/docs/ Fluid 官方文档 Hexo-theme-fluid https://github.com/fluid-dev/hexo-theme-fluid Fluid Github Repo Fluid Blog https://hexo.fluid-dev.com/ Fluid 官方博客
1
2
3
4
5
站点|地址|介绍
--|:--:|--:
Fluid Docs|https://hexo.fluid-dev.com/docs/|Fluid 官方文档
Hexo-theme-fluid|https://github.com/fluid-dev/hexo-theme-fluid|Fluid Github Repo
Fluid Blog|https://hexo.fluid-dev.com/|Fluid 官方博客
脚注
默认渲染器下正常显示,不同渲染器显示效果不同,写法如下:
1
2
脚注演示[^1]
[^1]: 脚注内容演示
善用 HTML
我们可以在 Markdown 中插入一些简单的 HTML 代码或 CSS 片段来获得更多扩展,使得文章内容更具有多样性。以下演示几个简单功能。
文字颜色
#519D9E颜色演示
1
<span style="color: #519D9E; ">#519D9E颜色演示</span>
文字大小
0.7em 文字大小演示
1
<span style="font-size:0.7em;">0.7em 文字大小演示</span>
文字位置
内容居中演示
1
<p style="text-align:center">内容居中演示</p> # 可以修改 text-align 参数来设置文字位置。
页内跳转
1
2
<a href="#demo">点击到达跳转位置演示</a> # 在需要跳转的地方添加此代码。
<a id="demo">跳转位置演示(跳转位置设置点)</a> # 在跳转位置添加次代码。
综合演示
综合演示
优雅使用 Fluid 写文章
1
2
3
4
5
6
<p style="text-align:center;color:#8EC0E4;font-size:1.5em;font-weight: bold;">
综合演示
<br>
优雅使用 Fluid 写文章
</p>
iframe 页面镶套
iframe 页面镶套可以帮助我们更好的展示一个页面。比如以下演示页面。
1
<iframe src="https://hexo.fluid-dev.com/" width="100%" height="500" name="topFrame" scrolling="yes" noresize="noresize" frameborder="0" id="topFrame"></iframe>
一些参数说明,width="100%"
为宽度自适应,高度请根据实际需求跳转,注意移动端页面是否匹配。 scrolling
为滚动条参数。frameborder
为边框参数。noresize
属性规定用户无法调整框架的大小。
善用 Tag 组件
Fluid 内置了许多 Tag 组件,包含便签、行内标签、勾选框、按钮和组图,可以使用这些组件来丰富文章内容,具体点击查看官方文档查看,点击跳转到 Fluid Doc。
配图
众所周知,博客好不好看,配图占一半。这里给大家推荐几个我常用找配图的地方。另外,请遵循对于网站的版权协议。
Wallpaper Hub

Wallhaven

Unsplash

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+搭配 Fluid 如何优雅的写一篇文章 - AIGISSS 搭配 Fluid 如何优雅的写一篇文章
本文最后更新于:6 个月前
本文由 Fluid 用户授权转载,版权归原作者所有。
本文作者:Vince
原文地址:https://i.vince.pub/posts/14677127/
前言
Fluid 是一款很十分优雅的主题,那么写一篇优雅的文章搭配它呢?以下会从几个方面来简述,主要还是做几个推荐。
文章内容
熟悉 Markdown 语法
对于使用 Hexo 的大多数人来说,相信对 Markdown 的语法不会陌生。熟练掌握 Markdown 语法对我们的写作拥有极大的帮助,这里用少用的表格和脚注来举个例子。至于为什么有些公式、流程图无法渲染,是因为 Markdown 追求简洁式写作,默认渲染器不支持复杂渲染。
表格
站点 地址 介绍 Fluid Docs https://hexo.fluid-dev.com/docs/ Fluid 官方文档 Hexo-theme-fluid https://github.com/fluid-dev/hexo-theme-fluid Fluid Github Repo Fluid Blog https://hexo.fluid-dev.com/ Fluid 官方博客
1
2
3
4
5
站点|地址|介绍
--|:--:|--:
Fluid Docs|https://hexo.fluid-dev.com/docs/|Fluid 官方文档
Hexo-theme-fluid|https://github.com/fluid-dev/hexo-theme-fluid|Fluid Github Repo
Fluid Blog|https://hexo.fluid-dev.com/|Fluid 官方博客
脚注
默认渲染器下正常显示,不同渲染器显示效果不同,写法如下:
1
2
脚注演示[^1]
[^1]: 脚注内容演示
善用 HTML
我们可以在 Markdown 中插入一些简单的 HTML 代码或 CSS 片段来获得更多扩展,使得文章内容更具有多样性。以下演示几个简单功能。
文字颜色
#519D9E颜色演示
1
<span style="color: #519D9E; ">#519D9E颜色演示</span>
文字大小
0.7em 文字大小演示
1
<span style="font-size:0.7em;">0.7em 文字大小演示</span>
文字位置
内容居中演示
1
<p style="text-align:center">内容居中演示</p> # 可以修改 text-align 参数来设置文字位置。
页内跳转
1
2
<a href="#demo">点击到达跳转位置演示</a> # 在需要跳转的地方添加此代码。
<a id="demo">跳转位置演示(跳转位置设置点)</a> # 在跳转位置添加次代码。
综合演示
综合演示
优雅使用 Fluid 写文章
1
2
3
4
5
6
<p style="text-align:center;color:#8EC0E4;font-size:1.5em;font-weight: bold;">
综合演示
<br>
优雅使用 Fluid 写文章
</p>
iframe 页面镶套
iframe 页面镶套可以帮助我们更好的展示一个页面。比如以下演示页面。
1
<iframe src="https://hexo.fluid-dev.com/" width="100%" height="500" name="topFrame" scrolling="yes" noresize="noresize" frameborder="0" id="topFrame"></iframe>
一些参数说明,width="100%"
为宽度自适应,高度请根据实际需求跳转,注意移动端页面是否匹配。 scrolling
为滚动条参数。frameborder
为边框参数。noresize
属性规定用户无法调整框架的大小。
善用 Tag 组件
Fluid 内置了许多 Tag 组件,包含便签、行内标签、勾选框、按钮和组图,可以使用这些组件来丰富文章内容,具体点击查看官方文档查看,点击跳转到 Fluid Doc。
配图
众所周知,博客好不好看,配图占一半。这里给大家推荐几个我常用找配图的地方。另外,请遵循对于网站的版权协议。
Wallpaper Hub

Wallhaven

Unsplash

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c8.html b/posts/9604e2c8.html
index a1b922df..f3325798 100644
--- a/posts/9604e2c8.html
+++ b/posts/9604e2c8.html
@@ -1,4 +1,4 @@
-在 Fluid 主题首页上加入一言 - AIGISSS 在 Fluid 主题首页上加入一言
本文最后更新于:6 个月前
群里有个小哥想在首页 Slogan 上显示一言,在 GitHub 上搜了搜,还真有 Fluid 主题的改造,我按照思路写下改造的步骤。
修改代码
typed.ejs
修改layout\_partial\plugins
目录下的typed.ejs
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
<% if(theme.fun_features.typing.enable && page.subtitle !== false){ %>
<%- js_ex(theme.static_prefix.typed, "/typed.min.js") %>
<script>
function typing(id, title){
var typed = new Typed('#' + id, {
strings: [
' ',
title + " ",
],
cursorChar: "<%- theme.fun_features.typing.cursorChar %>",
typeSpeed: <%- theme.fun_features.typing.typeSpeed %>,
loop: <%- theme.fun_features.typing.loop %>,
});
typed.stop();
$(document).ready(function () {
$(".typed-cursor").addClass("h2");
typed.start();
});
}
<% if(is_post()) { %>
typing("subtitle", "<%- data.subtitle %>")
<% } else if(theme.index.hitokoto.enable){ %>
fetch('https://v1.hitokoto.cn')
.then(response => response.json())
.then(data => {
typing("hitokoto", data.hitokoto)
})
.catch(console.error)
<% } else { %>
typing("subtitle", "<%- data.subtitle %>")
<% } %>
</script>
<% } %>
- 将原来的功能放在typing函数里面,再判断打字机显示subtitle还是hitokoto
- 所有的post都显示subtitle,即markdown的title,page的title是网站的标题
- 除了post以外,判断
theme.index.hitokoto.enable
- 设置显示一言,则通过fetch调用hitokoto的API,这个部分官方说明
在 Fluid 主题首页上加入一言 - AIGISSS 在 Fluid 主题首页上加入一言
本文最后更新于:6 个月前
群里有个小哥想在首页 Slogan 上显示一言,在 GitHub 上搜了搜,还真有 Fluid 主题的改造,我按照思路写下改造的步骤。
修改代码
typed.ejs
修改layout\_partial\plugins
目录下的typed.ejs
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
<% if(theme.fun_features.typing.enable && page.subtitle !== false){ %>
<%- js_ex(theme.static_prefix.typed, "/typed.min.js") %>
<script>
function typing(id, title){
var typed = new Typed('#' + id, {
strings: [
' ',
title + " ",
],
cursorChar: "<%- theme.fun_features.typing.cursorChar %>",
typeSpeed: <%- theme.fun_features.typing.typeSpeed %>,
loop: <%- theme.fun_features.typing.loop %>,
});
typed.stop();
$(document).ready(function () {
$(".typed-cursor").addClass("h2");
typed.start();
});
}
<% if(is_post()) { %>
typing("subtitle", "<%- data.subtitle %>")
<% } else if(theme.index.hitokoto.enable){ %>
fetch('https://v1.hitokoto.cn')
.then(response => response.json())
.then(data => {
typing("hitokoto", data.hitokoto)
})
.catch(console.error)
<% } else { %>
typing("subtitle", "<%- data.subtitle %>")
<% } %>
</script>
<% } %>
- 将原来的功能放在typing函数里面,再判断打字机显示subtitle还是hitokoto
- 所有的post都显示subtitle,即markdown的title,page的title是网站的标题
- 除了post以外,判断
theme.index.hitokoto.enable
- hitokoto比subtitle优先级高,这会导致归档、分类、标签等页面的打字机显示hitokoto
- 如果只需要在首页显示hitokoto,但在非post的页面显示原subtitle,这需要判断页面的属性,据我观察,所有的非post的页面布局(layout)都会设置
page.layout=”XXX“
,但是index和page没有设置,因此,可以通过!page.layout
判断来判断是否为首页,当然,post页面设定显示subtitle,就不在考虑范围内,这样只需将上面的else if
条件修改如下
1
<% } else if(theme.index.hitokoto.enable && !page.layout) { %>
layout.ejs
修改layout
目录下的layout.ejs
,在<span class="h2" id="subtitle">
和<% if(is_post()) { %>
之间插入如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<span class="h2" id="subtitle">
<% if(theme.fun_features.typing.enable == false) { %>
<%- subtitle %>
<% } %>
</span>
<% if(!is_post()) { %>
<br>
<span class="h2" id="hitokoto">
<% if(theme.fun_features.typing.enable == false) { %>
<%- hitokoto %>
<% } %>
</span>
<% } %>
<% if(is_post()) { %>
<%- partial('_partial/post-meta') %>
<% } %>
</div>
这个部分设置显示hitokoto的样式和位置,不设置这个会报关于typing("hitokoto", data.hitokoto)
的错误
1
TypeError: Cannot read property 'tagName' of null
修改配置
在source\_data
目录下修改主题配置文件fluid_config.yml
,在index
下设置hitokoto的开关
1
2
3
4
5
6
7
8
9
10
11
#---------------------------
# 首页
# Index Page
#---------------------------
index:
# 添加hitokoto
slogan: # 首页副标题的独立设置
enable: true # 为 false 则不显示任何内容
text: 'More haste, less speed.' # 为空则按 hexo config.subtitle 显示
hitokoto: # 非post页面显示一言
enable: true # slogan 和 hitokoto 不能同时启用,优先显示hitokoto
- 当
theme.index.hitokoto.enable == true
时,slogan里的text不在显示,因此只有关闭hitokoto才能在首页显示slogan的text或页面的subtitle
加入出处
- 如果想加入出处,可在打印
data.hitokoto
后加入data.from
,以及相应的格式
1
typing("hitokoto", '『' + data.hitokoto + '』' + '<br /> <h5>'+ '——' + '「' + data.from + '」' + '</h5>')
- 另一种显示出处的方法是另起一行打印
data.from
,
1
2
3
4
5
6
7
fetch('https://v1.hitokoto.cn')
.then(response => response.json())
.then(data => {
typing("hitokoto", data.hitokoto)
typing("hitofrom ", data.from)
})
.catch(console.error)
- 并在
layout.ejs
添加<%- hitofrom %>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<% if(!is_post()) { %>
<br>
<span class="h2" id="hitokoto">
<% if(theme.fun_features.typing.enable == false) { %>
<%- hitokoto %>
<% } %>
</span>
<br>
<span class="h2" id="hitofrom">
<% if(theme.fun_features.typing.enable == false) { %>
<%- hitofrom %>
<% } %>
</span>
<% } %>
- 第一种是打印一段话,从头到尾只有一个cursorChar,但样式不太好改
- 第二种是打印两段话,会出现视觉混乱(个人觉得),样式方便调整
总结
当然这个还可以继续改下去,例如添加出处(hitofrom)、设置循环(loop)、修改样式等。
最后也是最重要的,感谢tanxinzheng[3] (虽然不认识,但是新知识get!😁)。
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9604e2c9.html b/posts/9604e2c9.html
index 50604088..290ef3bb 100644
--- a/posts/9604e2c9.html
+++ b/posts/9604e2c9.html
@@ -1 +1 @@
-利用 GitHub Actions 自动部署 Hexo 博客 - AIGISSS 利用 GitHub Actions 自动部署 Hexo 博客
本文最后更新于:6 个月前
前言
本文主要讲如何应用于 Hexo 部署中,如果还不太熟悉 GitHub Actions 可以看这篇文章,简单地说 Actions 就是在设定的时机触发创建一个虚拟云环境,然后执行一连串动作,从而实现自动部署的功能。
创建工作流
首先要保证你的 Hexo 博客项目是全部提交到 GitHub 仓库中,然后在博客目录下创建 .github/workflows/xxx.yml
文件,文件名任意。
文件内容如下,根据自己的需求增删 step:
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
name: Deploy # Actions 显示的名字,随意设置
on: [push] # 监听到 push 事件后触发
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout # 拉取当前执行 Actions 仓库的指定分支
uses: actions/checkout@v2
with:
ref: master
- name: Update Submodule # 如果仓库有 submodule,在这里更新,没有则删掉此步骤
run: |
git submodule init
git submodule update --remote
- name: Setup Node # 安装 Node 环境
uses: actions/setup-node@v1
with:
node-version: "10.x"
- name: Hexo Generate # 安装 Hexo 依赖并且生成静态文件
run: |
rm -f .yarnclean
yarn --frozen-lockfile --ignore-engines --ignore-optional --non-interactive --silent --ignore-scripts --production=false
rm -rf ./public
yarn run hexo clean
yarn run hexo generate
- name: Hexo Deploy # 部署步骤,这里以 hexo deploy 为例
env:
SSH_PRIVATE: ${{ secrets.SSH_PRIVATE }}
GIT_NAME: yourname
GIT_EMAIL: your@email.com
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
yarn run hexo deploy
只要配置了 hexo deploy 的都可以通过上面这种方式部署,注意如果是在其他 Pages 部署(比如Coding Pages 或者 码云 Pages),ssh-keyscan
需要进行增改:
1
2
3
4
# github、gitee 和 coding 三种 Pages 的示例,根据需求替换上例中语句,需要注意的是 coding 是使用二级域名。
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-keyscan gitee.com >> ~/.ssh/known_hosts
ssh-keyscan e.coding.net >> ~/.ssh/known_hosts
然后 ${{ secrets.SSH_PRIVATE }}
这种调用方式,需要提前在下图中设置常量:

这样做可以避免敏感数据放在 yml 文件中被泄漏,即使你是私有仓库也建议这样做,因为设置的常量是无法被二次查看的,就算你账号被盗也不用担心。
常用步骤配置
以上是以部署 hexo deploy 为例,下面再提供几种其他常见的部署配置,注意修改你自己的变量参数。
阿里云 OSS
1
2
3
4
5
6
7
8
9
10
11
12
- name: Deploy to OSS
env:
OSS_AccessKeyID: ${{ secrets.ACCESS_KEY_ID }}
OSS_AccessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
OSS_EndPoint: oss-ap-southeast-1.aliyuncs.com
OSS_Bucket: fluid-dev
run: |
wget -q http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64
chmod +x ./ossutil64
./ossutil64 config -e $OSS_EndPoint -i $OSS_AccessKeyID -k $OSS_AccessKeySecret -L CH
./ossutil64 rm -r -f oss://$OSS_Bucket/
./ossutil64 cp -r -f ./public oss://$OSS_Bucket/
腾讯云 COS
1
2
3
4
5
6
7
8
- name: Deploy to COS
uses: zkqiang/tencent-cos-action@v0.1.0
with:
args: delete -r -f / && upload -r ./public/ /
secret_id: ${{ secrets.SECRET_ID }}
secret_key: ${{ secrets.SECRET_KEY }}
bucket: ${{ secrets.BUCKET }}
region: ap-shanghai
腾讯云开发
1
2
3
4
5
6
7
- name: Deploy to Tencent CloudBase
uses: TencentCloudBase/cloudbase-action@v1.1.1
with:
secretId: ${{ secrets.SECRET_ID }}
secretKey: ${{ secrets.SECRET_KEY }}
envId: ${{ secrets.ENV_ID }}
staticSrcPath: ./public
服务器
如果是直接部署在服务器上,需要通过 FTP/SFTP 协议来完成上传操作,因此确保你的服务器开启了 FTP 服务。
1
2
3
4
5
6
7
- name: Deploy to Server
uses: SamKirkland/FTP-Deploy-Action@3.1.1
with:
ftp-server: ${{ secrets.FTP_SERVER }} # eg: ftp://ftp.xxx.com:22/mypath
ftp-username: ${{ secrets.FTP_USERNAME }}
ftp-password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./public
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+利用 GitHub Actions 自动部署 Hexo 博客 - AIGISSS 利用 GitHub Actions 自动部署 Hexo 博客
本文最后更新于:6 个月前
前言
本文主要讲如何应用于 Hexo 部署中,如果还不太熟悉 GitHub Actions 可以看这篇文章,简单地说 Actions 就是在设定的时机触发创建一个虚拟云环境,然后执行一连串动作,从而实现自动部署的功能。
创建工作流
首先要保证你的 Hexo 博客项目是全部提交到 GitHub 仓库中,然后在博客目录下创建 .github/workflows/xxx.yml
文件,文件名任意。
文件内容如下,根据自己的需求增删 step:
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
name: Deploy # Actions 显示的名字,随意设置
on: [push] # 监听到 push 事件后触发
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout # 拉取当前执行 Actions 仓库的指定分支
uses: actions/checkout@v2
with:
ref: master
- name: Update Submodule # 如果仓库有 submodule,在这里更新,没有则删掉此步骤
run: |
git submodule init
git submodule update --remote
- name: Setup Node # 安装 Node 环境
uses: actions/setup-node@v1
with:
node-version: "10.x"
- name: Hexo Generate # 安装 Hexo 依赖并且生成静态文件
run: |
rm -f .yarnclean
yarn --frozen-lockfile --ignore-engines --ignore-optional --non-interactive --silent --ignore-scripts --production=false
rm -rf ./public
yarn run hexo clean
yarn run hexo generate
- name: Hexo Deploy # 部署步骤,这里以 hexo deploy 为例
env:
SSH_PRIVATE: ${{ secrets.SSH_PRIVATE }}
GIT_NAME: yourname
GIT_EMAIL: your@email.com
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
yarn run hexo deploy
只要配置了 hexo deploy 的都可以通过上面这种方式部署,注意如果是在其他 Pages 部署(比如Coding Pages 或者 码云 Pages),ssh-keyscan
需要进行增改:
1
2
3
4
# github、gitee 和 coding 三种 Pages 的示例,根据需求替换上例中语句,需要注意的是 coding 是使用二级域名。
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-keyscan gitee.com >> ~/.ssh/known_hosts
ssh-keyscan e.coding.net >> ~/.ssh/known_hosts
然后 ${{ secrets.SSH_PRIVATE }}
这种调用方式,需要提前在下图中设置常量:

这样做可以避免敏感数据放在 yml 文件中被泄漏,即使你是私有仓库也建议这样做,因为设置的常量是无法被二次查看的,就算你账号被盗也不用担心。
常用步骤配置
以上是以部署 hexo deploy 为例,下面再提供几种其他常见的部署配置,注意修改你自己的变量参数。
阿里云 OSS
1
2
3
4
5
6
7
8
9
10
11
12
- name: Deploy to OSS
env:
OSS_AccessKeyID: ${{ secrets.ACCESS_KEY_ID }}
OSS_AccessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
OSS_EndPoint: oss-ap-southeast-1.aliyuncs.com
OSS_Bucket: fluid-dev
run: |
wget -q http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64
chmod +x ./ossutil64
./ossutil64 config -e $OSS_EndPoint -i $OSS_AccessKeyID -k $OSS_AccessKeySecret -L CH
./ossutil64 rm -r -f oss://$OSS_Bucket/
./ossutil64 cp -r -f ./public oss://$OSS_Bucket/
腾讯云 COS
1
2
3
4
5
6
7
8
- name: Deploy to COS
uses: zkqiang/tencent-cos-action@v0.1.0
with:
args: delete -r -f / && upload -r ./public/ /
secret_id: ${{ secrets.SECRET_ID }}
secret_key: ${{ secrets.SECRET_KEY }}
bucket: ${{ secrets.BUCKET }}
region: ap-shanghai
腾讯云开发
1
2
3
4
5
6
7
- name: Deploy to Tencent CloudBase
uses: TencentCloudBase/cloudbase-action@v1.1.1
with:
secretId: ${{ secrets.SECRET_ID }}
secretKey: ${{ secrets.SECRET_KEY }}
envId: ${{ secrets.ENV_ID }}
staticSrcPath: ./public
服务器
如果是直接部署在服务器上,需要通过 FTP/SFTP 协议来完成上传操作,因此确保你的服务器开启了 FTP 服务。
1
2
3
4
5
6
7
- name: Deploy to Server
uses: SamKirkland/FTP-Deploy-Action@3.1.1
with:
ftp-server: ${{ secrets.FTP_SERVER }} # eg: ftp://ftp.xxx.com:22/mypath
ftp-username: ${{ secrets.FTP_USERNAME }}
ftp-password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./public
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/9c1bf78f.html b/posts/9c1bf78f.html
index e87a4917..bc59cc98 100644
--- a/posts/9c1bf78f.html
+++ b/posts/9c1bf78f.html
@@ -1 +1 @@
-Mapbox的表达式 - AIGISSS Mapbox的表达式
本文最后更新于:8 个月前
Expressions
mapbox-gl 的表达式可以将任何布局layout
属性,绘图paint
属性或过滤器filter
的值指定为表达式。
图层样式结构
图层样式设置的结构为 JSON 结构,根级别结构如下:
1
2
3
4
5
6
7
8
9
10
11
{
"id": "road-layer-1",
"type": "line",
"source": "RoadSource",
"source-layer": "Road",
"layout": {...},
"paint": {...},
"minzoom": 6,
"maxzoom": 17.5,
"filter": [...]
}
字段定义说明如下:
id:图层 ID,必填项;说明:(值唯一,不能重复)
type:图层渲染类型,必填项;(值域范围,参考本章「概述」)
source:所使用的数据源 ID,说明:(当图层类型不为 background 时,该值为必填项)
source-layer:所使用的 vector 数据源中的图层标识,说明:(当数据源类型为 vector 时,该值为必填项;其它数据源类型,去除该参数)
layout:布局属性
paint:绘制属性
minzoom:图层可展示的最小缩放等级值,选填项;说明:(值为小数或整数,值域范围[0,24],不设置该参数则地图允许的最小缩放等级内都可显示)
maxzoom:图层可展示的最大缩放等级值,选填项;说明:(值为小数或整数,值域范围[0,24],不设置该参数则地图允许的最大缩放等级内都可显示)
filter:所使用数据源的 features 数据的过滤条件,选填项;说明:(当数据源 features 数据信息和 filter 条件匹配时图层才显示)
表达式定义了一个公式,用于使用以下描述的运算符计算属性的值。Mapbox GL 提供的表达式运算符集包括:
- Mathematical operators:数学计算器用于数值计算和其他数值相关的属性(如’+’ ‘-‘ ‘*‘ ‘/‘)
- Logical operators:用于操纵布尔值和进行条件决策的逻辑运算符(如’case’ ‘let’)
- String operators:用于操纵字符串的字符串运算符(如 string 转 number)
- Data operators:提供调用数据源要素集属性的接口(如 ‘get’)
- Camera operators:提供定义当前地图视角参数的接口(如 ‘zoom’)
Expressions 表达式使用类似 Lisp 的语法,表达式数组的第一个元素是一个表示计算器的字符串,例如*
或case
。后面的元素是表达式的参数,每个参数要么是个原始的值(字符串、数字、布尔值或 null),要么是另一个表达式数组。
1
[expression_name, argument_0, argument_1, ...]
Data expressions
数据表达式是任何能够调用要素数据的表达式,这种表达式使用如下一种数据计算器:get
、has
、id
、geometry-type
、properties
、or feature-state
。数据表达式利用要素集的属性或者状态来决定如何表达要素,它们可以在同一图层中创建不同的数据表达。
1
2
3
4
5
6
7
8
9
10
11
{
"circle-color": [
"rgb",
// red is higher when feature.properties.temperature is higher
["get", "temperature"],
// green is always zero
0,
// blue is higher when feature.properties.temperature is lower
["-", 100, ["get", "temperature"]]
]
}
上面这个例子使用get
运算符来获取每个要素的温度值,这个结果值被用作 rgb 运算符的属性值,rgb 运算符是用红绿蓝定义颜色的操作符。
数据表达式可以被用作 filter 的属性值和大多数布局layout
属性、绘画paint
属性值的计算。然而,一些绘画paint
和布局layout
属性并不支持数据表达式。支持级别可以在支持列表中查看。feature-state
运算符的数据表达式仅适用于绘画paint
属性中。
Camera expressions
一个相机表达式是指任何使用 zoom 操作符的表达式,这些表达式允许一个图层改变地图的缩放等级。相机表达式可以用来创建表现的深度和控制数据密度。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
// zoom is 5 (or less) -> circle radius will be 1px
5,
1,
// zoom is 10 (or greater) -> circle radius will be 5px
10,
5
]
}
这个例子使用interpolate
插值运算符用来定义缩放等级和圆大小的线性相对关系,在这个例子中,表达式表明了圆半径在 zoom 等级为 5 或者更低时为 1 像素,在 zoom 等级为 10 或者更高时为 5 像素。在此之间的 zoom,半径线性的在 1 到 5 像素之间变化。
相机表达式在任何表达式使用的地方都能使用,然后当相机表达式用于布局layout
属性或者绘画paint
属性的值时,它必须是以下面形式中的一种:
1
[ "interpolate", interpolation, ["zoom"], ... ]
或者:
1
[ "step", ["zoom"], ... ]
或者:
1
2
3
4
5
[
"let",
... variable bindings...,
[ "interpolate", interpolation, ["zoom"], ... ]
]
或者:
1
2
3
4
5
[
"let",
... variable bindings...,
[ "step", ["zoom"], ... ]
]
也就是说,用于布局layout
属性和绘画paint
属性时,zoom
操作符只能被用作为interpolate
、step
或者let
这三种操作符的内在操作符。在布局layout
属性和绘画paint
属性中使用相机表达式有个重要的区别,绘画paint
属性相机表达式当 zoom 等级发生即使很小变化时就能够重绘,例如一个绘画paint
属性相机表达式会持续的变化,当 zoom 等级在 4.1 和 4.6 之间变动时。与此同时,布局layout
属性相机表达式只会在 zoom 整数跳变时计算值,例如在 4.1 到 4.6 之间变化时不会重新计算值,除非是重 5 到 4.
Composition
一个单独的表达式可能使用 data 操作符、相机操作符和其他操作符的混合。这种组合表达式使一个图层的渲染决定于缩放等级和单独要素属性的组合。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
// when zoom is 0, set each feature's circle radius to the value of its "rating" property
0,
["get", "rating"],
// when zoom is 10, set each feature's circle radius to four times the value of its "rating" property
10,
["*", 4, ["get", "rating"]]
]
}
一个同时使用了 data 和 camera 运算符的表达式同时考虑了 data 和 camera 表达式。
http://dev.minedata.cn/api/dev/js/guide/layer/style
filter 规格
fliter 表示从所有图层过滤出特定特征的图层,有以下几种过滤形式:
1、存在过滤:
表达式形式:[way, key]
1
2
3
4
存在过滤主要有“has”、“!has”两种形式
["has", "count"],表示过滤出存在属性"count"的所有的feature数据
["!has","count"],表示过滤出不存在属性"count"的所有的feature数据
2、比较过滤:
表达式形式:[way, key, value]
1
2
3
4
比较过滤有等于“==”、大于“>”、小于“<”、不等于“!=”、大于等于“>=”、小于等于“<=”几种形式
["==", "count", "1000"],表示过滤出属性"count"值为1000的feature数据,
// 注意此时数据源name存储的值为"1000"而不是1000 过滤时数据类型是严格匹配的
3、成员过滤:
计算形式:[way, key, v0,v1,…,vn]
1
2
3
4
成员过滤主要有"in"、"!in"两种形式
["in", "name", "point", "fill", "line"],
// 表示过滤出属性"name"值为"point", "fill", "line"其中任一一个的feature数据
4、组过滤:
计算形式:[way, key, v0,v1,…,vn]
1
2
3
4
5
组过滤主要有组包含"arrin"、组不包含"!arrin"两种形式
["arrin", "name", "point", "fill"],
// 表示过滤出属性"name"值为["point", "fill"]、["point"]、["fill"]其中任一一个的feature数据
// 属性"name"值为数组形式
5、模糊过滤:
计算形式:[way, key, value]
1
2
3
4
模糊过滤有“like”、开始于“start”、结束于“end”几种形式
["like", "code", "101"],表示过滤出属性"code"值包含101的feature数据
// 属性"code"值为字符形式
6、组合过滤:
计算形式:[way, f0,f1,…,fn]
1
2
3
4
5
组合过滤主要有等于"all"、"any"、"none"三种形式
"all"表示满足所有过滤条件的数据,"any"表示满足任一一个过滤条件的数据,"none"表示过滤出不满足所有过滤条件的数据
["all", ["<=", "count", 34], ["like", "code", "101"]],
// 表示过滤出属性"count"满足大于等于34,属性"code"值包含101的feature数据
函数对象语句
图层样式的某些 layout 或 paint 属性值支持函数对象语句的方式,属性值的最终结果由当前缩放等级或 feature 属性值进行相关计算而取得。
函数对象语句的结构为 JSON 结构,根级别结构如下:
1
2
3
4
5
6
7
{
"property": "kind",
"base": 1,
"type": "interval",
"default": "#000000",
"stops": [...]
}
字段定义说明如下:
property:具体的 feature 属性标识;(非必输项)
base:差值运算的曲率指数基数,控制最终计算结果值的增长率,值越大,最终计算结果值越大,值为 1 时,函数采用线性计算方式;(数值类型,默认值为 1)
stops:差值运算项,定义输入值和输出值的集合,每一个 stop 由一个输入值和一个输出值组成;(数组形式,当 type 不为’identity’时,stops 为必输项)
type:函数对象计算类型;(值域为[“identity”, “exponential”,”interval”, “categorical”],默认值为’interval’)
1、identity:恒等类型,最终输出值完全等于输入值;
1
2
3
4
5
6
7
/*示例:建筑物高度取用建筑物feature中的的属性字段levels的值*/
{
"extrusion-height": {
"type": "identity",
"property": "levels"
}
}
2、exponential:指数类型,最终输出值由 stops 中的差值项进行区间范围内的指数级差值计算生成,stops 中的输入参数必须为数值类型;
1
2
3
4
5
6
7
8
9
/*示例:线的颜色值由feature中的price值进行区间指数级差值运算取得*/
{
"line-width": {
"property": "price",
"type": "exponential",
"stops": [[0, 1],[10, 2],[200, 3],[300, 4]],
"default": 1
}
}
3、interval:区间类型,最终输出值由 stops 中的差值项进行区间范围内的阶梯型差值计算生成,stops 中的输入参数必须为数值类型;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*示例:线的颜色值由feature中的status值进行区间差值运算取得*/
{
"line-color": {
"property": "status",
"type": "interval",
"stops": [[0, "#999999"],[1, "#66cc00"],[2, "#ff9900"],[3, "#cc0000"],[4, "#9d0404"]],
"default": "#66cc00"
}
}
/*示例:表示线宽根据zoom的值进行差值运算,当不添加type属性时,默认为interval类型*/
{
"line-width": {
"base": 1.2,
"stops": [[5, 0.8], [20, 6]]
}
}
4、categorical:种别类型,最终输出值完全匹配 stops 中的输入值对应的输出值;
1
2
3
4
5
6
7
8
9
/*示例:面颜色由feature中的的属性字段space_type的值匹配stop中的输入值取得输出值*/
{
"line-color": {
"property": "space_type",
"type": "categorical",
"stops": [[1, "#f8e4d4"], [3, "#f5e8ca"], [5, "#f1d4ef"], [7, "#f7e8c3"], [9, "#f0d3ef"]]
"default": "#f1ebe7"
}
}
default:默认值,当差值运算没有结果时取用默认值;会在以下境况中遇到:
1、函数对象为categorical类型:当 feature 属性值不匹配 stops 中的输入值时;
2、函数对象为identity类型:当 feature 属性值不存在或属性值无效时;
3、函数对象为interval 或 exponential类型:当 feature 属性值不是数值类型时;
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+Mapbox的表达式 - AIGISSS Mapbox的表达式
本文最后更新于:8 个月前
Expressions
mapbox-gl 的表达式可以将任何布局layout
属性,绘图paint
属性或过滤器filter
的值指定为表达式。
图层样式结构
图层样式设置的结构为 JSON 结构,根级别结构如下:
1
2
3
4
5
6
7
8
9
10
11
{
"id": "road-layer-1",
"type": "line",
"source": "RoadSource",
"source-layer": "Road",
"layout": {...},
"paint": {...},
"minzoom": 6,
"maxzoom": 17.5,
"filter": [...]
}
字段定义说明如下:
id:图层 ID,必填项;说明:(值唯一,不能重复)
type:图层渲染类型,必填项;(值域范围,参考本章「概述」)
source:所使用的数据源 ID,说明:(当图层类型不为 background 时,该值为必填项)
source-layer:所使用的 vector 数据源中的图层标识,说明:(当数据源类型为 vector 时,该值为必填项;其它数据源类型,去除该参数)
layout:布局属性
paint:绘制属性
minzoom:图层可展示的最小缩放等级值,选填项;说明:(值为小数或整数,值域范围[0,24],不设置该参数则地图允许的最小缩放等级内都可显示)
maxzoom:图层可展示的最大缩放等级值,选填项;说明:(值为小数或整数,值域范围[0,24],不设置该参数则地图允许的最大缩放等级内都可显示)
filter:所使用数据源的 features 数据的过滤条件,选填项;说明:(当数据源 features 数据信息和 filter 条件匹配时图层才显示)
表达式定义了一个公式,用于使用以下描述的运算符计算属性的值。Mapbox GL 提供的表达式运算符集包括:
- Mathematical operators:数学计算器用于数值计算和其他数值相关的属性(如’+’ ‘-‘ ‘*‘ ‘/‘)
- Logical operators:用于操纵布尔值和进行条件决策的逻辑运算符(如’case’ ‘let’)
- String operators:用于操纵字符串的字符串运算符(如 string 转 number)
- Data operators:提供调用数据源要素集属性的接口(如 ‘get’)
- Camera operators:提供定义当前地图视角参数的接口(如 ‘zoom’)
Expressions 表达式使用类似 Lisp 的语法,表达式数组的第一个元素是一个表示计算器的字符串,例如*
或case
。后面的元素是表达式的参数,每个参数要么是个原始的值(字符串、数字、布尔值或 null),要么是另一个表达式数组。
1
[expression_name, argument_0, argument_1, ...]
Data expressions
数据表达式是任何能够调用要素数据的表达式,这种表达式使用如下一种数据计算器:get
、has
、id
、geometry-type
、properties
、or feature-state
。数据表达式利用要素集的属性或者状态来决定如何表达要素,它们可以在同一图层中创建不同的数据表达。
1
2
3
4
5
6
7
8
9
10
11
{
"circle-color": [
"rgb",
// red is higher when feature.properties.temperature is higher
["get", "temperature"],
// green is always zero
0,
// blue is higher when feature.properties.temperature is lower
["-", 100, ["get", "temperature"]]
]
}
上面这个例子使用get
运算符来获取每个要素的温度值,这个结果值被用作 rgb 运算符的属性值,rgb 运算符是用红绿蓝定义颜色的操作符。
数据表达式可以被用作 filter 的属性值和大多数布局layout
属性、绘画paint
属性值的计算。然而,一些绘画paint
和布局layout
属性并不支持数据表达式。支持级别可以在支持列表中查看。feature-state
运算符的数据表达式仅适用于绘画paint
属性中。
Camera expressions
一个相机表达式是指任何使用 zoom 操作符的表达式,这些表达式允许一个图层改变地图的缩放等级。相机表达式可以用来创建表现的深度和控制数据密度。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
// zoom is 5 (or less) -> circle radius will be 1px
5,
1,
// zoom is 10 (or greater) -> circle radius will be 5px
10,
5
]
}
这个例子使用interpolate
插值运算符用来定义缩放等级和圆大小的线性相对关系,在这个例子中,表达式表明了圆半径在 zoom 等级为 5 或者更低时为 1 像素,在 zoom 等级为 10 或者更高时为 5 像素。在此之间的 zoom,半径线性的在 1 到 5 像素之间变化。
相机表达式在任何表达式使用的地方都能使用,然后当相机表达式用于布局layout
属性或者绘画paint
属性的值时,它必须是以下面形式中的一种:
1
[ "interpolate", interpolation, ["zoom"], ... ]
或者:
1
[ "step", ["zoom"], ... ]
或者:
1
2
3
4
5
[
"let",
... variable bindings...,
[ "interpolate", interpolation, ["zoom"], ... ]
]
或者:
1
2
3
4
5
[
"let",
... variable bindings...,
[ "step", ["zoom"], ... ]
]
也就是说,用于布局layout
属性和绘画paint
属性时,zoom
操作符只能被用作为interpolate
、step
或者let
这三种操作符的内在操作符。在布局layout
属性和绘画paint
属性中使用相机表达式有个重要的区别,绘画paint
属性相机表达式当 zoom 等级发生即使很小变化时就能够重绘,例如一个绘画paint
属性相机表达式会持续的变化,当 zoom 等级在 4.1 和 4.6 之间变动时。与此同时,布局layout
属性相机表达式只会在 zoom 整数跳变时计算值,例如在 4.1 到 4.6 之间变化时不会重新计算值,除非是重 5 到 4.
Composition
一个单独的表达式可能使用 data 操作符、相机操作符和其他操作符的混合。这种组合表达式使一个图层的渲染决定于缩放等级和单独要素属性的组合。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
// when zoom is 0, set each feature's circle radius to the value of its "rating" property
0,
["get", "rating"],
// when zoom is 10, set each feature's circle radius to four times the value of its "rating" property
10,
["*", 4, ["get", "rating"]]
]
}
一个同时使用了 data 和 camera 运算符的表达式同时考虑了 data 和 camera 表达式。
http://dev.minedata.cn/api/dev/js/guide/layer/style
filter 规格
fliter 表示从所有图层过滤出特定特征的图层,有以下几种过滤形式:
1、存在过滤:
表达式形式:[way, key]
1
2
3
4
存在过滤主要有“has”、“!has”两种形式
["has", "count"],表示过滤出存在属性"count"的所有的feature数据
["!has","count"],表示过滤出不存在属性"count"的所有的feature数据
2、比较过滤:
表达式形式:[way, key, value]
1
2
3
4
比较过滤有等于“==”、大于“>”、小于“<”、不等于“!=”、大于等于“>=”、小于等于“<=”几种形式
["==", "count", "1000"],表示过滤出属性"count"值为1000的feature数据,
// 注意此时数据源name存储的值为"1000"而不是1000 过滤时数据类型是严格匹配的
3、成员过滤:
计算形式:[way, key, v0,v1,…,vn]
1
2
3
4
成员过滤主要有"in"、"!in"两种形式
["in", "name", "point", "fill", "line"],
// 表示过滤出属性"name"值为"point", "fill", "line"其中任一一个的feature数据
4、组过滤:
计算形式:[way, key, v0,v1,…,vn]
1
2
3
4
5
组过滤主要有组包含"arrin"、组不包含"!arrin"两种形式
["arrin", "name", "point", "fill"],
// 表示过滤出属性"name"值为["point", "fill"]、["point"]、["fill"]其中任一一个的feature数据
// 属性"name"值为数组形式
5、模糊过滤:
计算形式:[way, key, value]
1
2
3
4
模糊过滤有“like”、开始于“start”、结束于“end”几种形式
["like", "code", "101"],表示过滤出属性"code"值包含101的feature数据
// 属性"code"值为字符形式
6、组合过滤:
计算形式:[way, f0,f1,…,fn]
1
2
3
4
5
组合过滤主要有等于"all"、"any"、"none"三种形式
"all"表示满足所有过滤条件的数据,"any"表示满足任一一个过滤条件的数据,"none"表示过滤出不满足所有过滤条件的数据
["all", ["<=", "count", 34], ["like", "code", "101"]],
// 表示过滤出属性"count"满足大于等于34,属性"code"值包含101的feature数据
函数对象语句
图层样式的某些 layout 或 paint 属性值支持函数对象语句的方式,属性值的最终结果由当前缩放等级或 feature 属性值进行相关计算而取得。
函数对象语句的结构为 JSON 结构,根级别结构如下:
1
2
3
4
5
6
7
{
"property": "kind",
"base": 1,
"type": "interval",
"default": "#000000",
"stops": [...]
}
字段定义说明如下:
property:具体的 feature 属性标识;(非必输项)
base:差值运算的曲率指数基数,控制最终计算结果值的增长率,值越大,最终计算结果值越大,值为 1 时,函数采用线性计算方式;(数值类型,默认值为 1)
stops:差值运算项,定义输入值和输出值的集合,每一个 stop 由一个输入值和一个输出值组成;(数组形式,当 type 不为’identity’时,stops 为必输项)
type:函数对象计算类型;(值域为[“identity”, “exponential”,”interval”, “categorical”],默认值为’interval’)
1、identity:恒等类型,最终输出值完全等于输入值;
1
2
3
4
5
6
7
/*示例:建筑物高度取用建筑物feature中的的属性字段levels的值*/
{
"extrusion-height": {
"type": "identity",
"property": "levels"
}
}
2、exponential:指数类型,最终输出值由 stops 中的差值项进行区间范围内的指数级差值计算生成,stops 中的输入参数必须为数值类型;
1
2
3
4
5
6
7
8
9
/*示例:线的颜色值由feature中的price值进行区间指数级差值运算取得*/
{
"line-width": {
"property": "price",
"type": "exponential",
"stops": [[0, 1],[10, 2],[200, 3],[300, 4]],
"default": 1
}
}
3、interval:区间类型,最终输出值由 stops 中的差值项进行区间范围内的阶梯型差值计算生成,stops 中的输入参数必须为数值类型;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*示例:线的颜色值由feature中的status值进行区间差值运算取得*/
{
"line-color": {
"property": "status",
"type": "interval",
"stops": [[0, "#999999"],[1, "#66cc00"],[2, "#ff9900"],[3, "#cc0000"],[4, "#9d0404"]],
"default": "#66cc00"
}
}
/*示例:表示线宽根据zoom的值进行差值运算,当不添加type属性时,默认为interval类型*/
{
"line-width": {
"base": 1.2,
"stops": [[5, 0.8], [20, 6]]
}
}
4、categorical:种别类型,最终输出值完全匹配 stops 中的输入值对应的输出值;
1
2
3
4
5
6
7
8
9
/*示例:面颜色由feature中的的属性字段space_type的值匹配stop中的输入值取得输出值*/
{
"line-color": {
"property": "space_type",
"type": "categorical",
"stops": [[1, "#f8e4d4"], [3, "#f5e8ca"], [5, "#f1d4ef"], [7, "#f7e8c3"], [9, "#f0d3ef"]]
"default": "#f1ebe7"
}
}
default:默认值,当差值运算没有结果时取用默认值;会在以下境况中遇到:
1、函数对象为categorical类型:当 feature 属性值不匹配 stops 中的输入值时;
2、函数对象为identity类型:当 feature 属性值不存在或属性值无效时;
3、函数对象为interval 或 exponential类型:当 feature 属性值不是数值类型时;
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/a2d9d205.html b/posts/a2d9d205.html
index 9cb46463..9c7432df 100644
--- a/posts/a2d9d205.html
+++ b/posts/a2d9d205.html
@@ -1 +1 @@
-mapboxgl实现marker的聚类 - AIGISSS mapboxgl实现marker的聚类
本文最后更新于:8 个月前

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+mapboxgl实现marker的聚类 - AIGISSS mapboxgl实现marker的聚类
本文最后更新于:1 分钟前

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/a70e035f.html b/posts/a70e035f.html
index 9f8424fd..540904ec 100644
--- a/posts/a70e035f.html
+++ b/posts/a70e035f.html
@@ -1 +1 @@
-centos7下编译安装postgis3 - AIGISSS centos7下编译安装postgis3
本文最后更新于:5 个月前
本文认为已安装PostgreSQL12,安装步骤如 Centos7安装PostgreSQL,最好按照前文先把pg安装好,否则,在postgis,pgrouting安装时,指定pg的安装目录,直接抄路径应该不对,读者要指向自己的安装位置等。
安装前准备
升级cmake
CGAL4-11。因为4-11需要CMake3.11以上
1
2
cmake -version
# cmake version 2.8.12.2
1
2
3
4
5
6
wget https://github.com/Kitware/CMake/releases/download/v3.16.8/cmake-3.16.8.tar.gz
tar -zxvf cmake-3.16.8.tar.gz
cd cmake-3.16.8
./bootstrap --prefix=/usr/local/cmake
gamke
gmake install
postgis
还要安装其他依赖,比如GEOS
,proj
,GDAL
,各版本需要的依赖版本,http://postgis.net/news/ ,如图。

本文基于GEOS 3.8.1,PROJ 6.3.2,GDAL 3.0.4,json-c 0.13.1,CGAL4.14.3,SFCGAL1.3.7,protobuf3.11.4,protobuf-c1.3.3,libxml2-2.9.10,pcre-8.44,PostGIS-3.0.1,pgrouting3.0.2安装。
安装依赖或插件
安装GEOS-3.8.1
1
2
3
4
5
wget https://download.osgeo.org/geos/geos-3.8.1.tar.bz2
tar -jxf geos-3.8.1.tar.bz2
cd geos-3.8.1
./configure --prefix=/usr/local/geos-3.8.1
make && make install
:blush:
安装proj-6.3.2
1
2
3
4
5
6
wget http://download.osgeo.org/proj/proj-6.3.2.tar.gz
tar -zxvf proj-6.3.2.tar.gz
cd proj-6.3.2
./configure --prefix=/usr/local/proj-6.3.2
# 编译时遇到下面的问题,说是sqlite版本太低!编译成功再进行下一步!
make && make install
遇到如图片的问题!

然后我在命令行输入sqlite3的时候,结果是3.22,哪来的3.7.17?

原来是sqlite-devel的版本,但我查询之后是最新的!!于是我删了sqlite-devl,得到的是Checking for module 'sqlite3' No package 'sqlite3' found
暴力升级也没有用,不得不看日志的下半部分。于是搜索关键词PKG_CONFIG_PATH
。找到https://my.oschina.net/zzop/blog/499908这篇文章,其中说到:

:point_right:

cp /usr/local/lib/pkgconfig/sqlite3.pc /usr/lib64/pkgconfig/
重新编译就好了!

安装GDAL
1
2
3
4
5
6
7
8
wget https://download.osgeo.org/gdal/3.0.4/gdal-3.0.4.tar.gz
tar -zxvf gdal-3.0.4.tar.gz
cd gdal-3.0.4
# 带pg的配置,具体需要啥可以./configure --help 查看
./configure --prefix=/usr/local/gdal-3.0.4 --with-pg=yes
make
# 编译时遇到下面的问题,成功则进行下一步!
make install
遇到
1
2
3
collect2: error: ld returned 1 exit status
make[1]: *** [GNUmakefile:82: gdalinfo] Error 1
make: *** [GNUmakefile:112: apps-target] Error 2
解决办法可能是make clean
,紧接着按之前的编译安装!
参考文章:https://stackoverflow.com/questions/60218227/trying-to-install-gdal-3-0-4-on-red-hat-8
我的编译配置:
1
./configure --prefix=/usr/local/gdal-3.0.4 --with-proj=/usr/local/proj-6.3.2 --with-geos=/usr/local/geos-3.8.1/bin/geos-config --with-sqlite3=/usr/local/bin/sqlite3 --with-libjson-c=/usr/local/json-c-0.13.1 --with-pg=yes --with-python=/root/.virtualenvs/aigisss_py/bin/python3.6
安装protubuf
1
2
3
tar -xzvf protobuf-cpp-3.11.4.tar.gz
./configure --prefix=/usr/local/protobuf-3.11.4
make && make install
竟然说bash: ./configure: No such file or directory
解决方案
1
2
yum install automake
autoreconf -i
参考文章:https://stackoverflow.com/questions/24054761/configure-gives-error-in-ubuntu
出现编译错误
1
2
3
make[2]: *** [message.lo] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2
解决方案下载含有all的资源包

参考文章:https://github.com/protocolbuffers/protobuf/issues/6599
安装protobuf-c
1
2
3
4
5
6
wget https://github.com/protobuf-c/protobuf-c/releases/download/v1.3.3/protobuf-c-1.3.3.tar.gz
tar -xzvf protobuf-c-1.3.3.tar.gz
cd protobuf-c-1.3.3
export PKG_CONFIG_PATH=/usr/local/protobuf-3.11.4/lib/pkgconfig
./configure --prefix=/usr/local/protobuf-c-1.3.3
make && make install
安装SFCGAL 1.3.7
由于SFCGAL需要依赖Boost、CGAL、GMP、MPFR这四个软件,所以具体总共需要安装以下四个软件:
boost-devel.x86_64
gmp-devel.x86_64
mpfr-devel.x86_64
CGAL-4.14
为了安装pgrouting3.0.2
需要安装boost1.53
以上,使用yum install boost boost-devel
只能安装版本1.53
, 我使用源码安装的是1.68
1
2
3
4
5
wget https://dl.bintray.com/boostorg/release/1.68.0/source/boost_1_68_0.tar.gz
tar -xzvf boost_1_68_0.tar.gz
cd boost_1_68_0
./bootstrap.sh
./b2 install --with=all
1
2
yum install gmp-devel.x86_64
yum install mpfr-devel.x86_64
安装CGAL
1
2
3
4
5
6
7
wget http://distfiles.macports.org/cgal/cgal-4.14.3.tar.xz
xz -d CGAL-4.14.3.tar.xz
tar -xvf CGAL-4.14.3.tar
cd CGAL-4.14.3
mkdir build && cd build
cmake ..
make && make install
安装pcre
1
2
3
4
5
6
7
wget https://ftp.pcre.org/pub/pcre/pcre-8.44.tar.gz
tar -xzvf pcre-8.44.tar.gz
cd pcre-8.44
./configure --enable-utf8 --prefix=/usr/local/pcre-8.44
make && make intall
echo "/usr/local/pcre/lib" > /etc/ld.so.conf.d/pcre-8.44.conf
ldconfig
安装SFCGAL
遇到以下这个问题:
1
2
c++: internal compiler error: Killed (program cc1plus)
Please submit a full bug report
解决方案:
1
2
3
4
5
6
7
8
sudo dd if=/dev/zero of=/swapfile bs=64M count=16
#count的大小就是增加的swap空间的大小,64M是块大小,所以空间大小是bs*count=1024MB
sudo mkswap /swapfile
#把刚才空间格式化成swap格式
chmod 0600 /swapfile
#该目录权限,不改的话,在下一步启动时会报“swapon: /swapfile: insecure permissions 0644, 0600 suggested.”错误
sudo swapon /swapfile
#使用刚才创建的swap空间
安装编译:
1
2
3
4
5
6
wget https://github.com/Oslandia/SFCGAL/archive/v1.3.7.tar.gz
tar -zxvf SFCGAL-1.3.7.tar.gz
cd SFCGAL-1.3.7
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local/sfcgal-1.3.7 ..
make && make install
装完之后释放空间:
1
2
3
swapoff -a
#详细的用法可以:swapoff --help
#查看当前内存使用情况:free -m
参考文章:https://blog.csdn.net/qq_27148893/article/details/88936044
安装PostGIS
1
./configure --prefix=/usr/local/postgis-3.0.1 --with-gdalconfig=/usr/local/gdal-3.0.4/bin/gdal-config --with-pgconfig=/opt/pg12/bin/pg_config --with-geosconfig=/usr/local/geos-3.8.1/bin/geos-config --with-projdir=/usr/local/proj-6.3.2 --with-xml2config=/usr/local/libxml2-2.9.10/bin/xml2-config --with-jsondir=/usr/local/json-c-0.13.1 --with-protobufdir=/usr/local/protobuf-c-1.3.3 --with-sfcgal=/usr/local/sfcgal-1.3.7/bin/sfcgal-config --with-pcredir=/usr/local/pcre-8.44
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PG_HOME=/opt/pg12
LD_LIBRARY_PATH=$PG_HOME/lib:$LD_LIBRARY_PATH
PATH=$PG_HOME/bin:$PATH
PKG_CONFIG_PATH=$PG_HOME/lib/pkgconfig:$PKG_CONFIG_PATH
export CMAKE_HOME=/usr/bin/cmake
export PROTOBUF_HOME=/usr/local/protobuf-3.11.4
GDAL_HOME=/usr/local/gdal-3.0.4
GDAL_DATA=$GDAL_HOME/share/gdal
LD_LIBRARY_PATH=$GDAL_HOME/lib:/usr/local/lib64:$JRE_HOME/lib:$LD_LIBRARY_PATH
PATH=$GDAL_HOME/bin:$PATH
export PATH=$CMAKE_HOME/bin:$PROTOBUF_HOME/bin:/usr/local/protobuf-c-1.3.3/bin:$PATH
export PKG_CONFIG_PATH LD_LIBRARY_PATH
export LD_LIBRARY_PATH GDAL_DATA
创建postgis扩展
1
2
3
4
5
6
7
su - postgres
psql
create database gistest;
\c gistest
create extension postgis;
#如果安装了sfcgal,创建扩展测试下
create extension postgis_sfcgal;
安装PgRouting
1
2
3
4
5
6
7
8
9
wget https://github.com/pgRouting/pgrouting/releases/download/v3.0.2/pgrouting-3.0.2.tar.gz
tar -zxvf pgrouting-3.0.2.tar.gz
cd pgrouting-3.0.2
mkdir build && cd build
#引入postgres的环境变量
source /home/postgres/.bashrc
cmake ..
make
make install
验证安装:
1
2
3
4
5
su - postgres
psql
create database gistest;
\c gistest
create extension pgrouting;
验证:sql查询
1
SELECT ST_AsX3D(ST_Extrude(ST_Buffer(ST_GeomFromText('POINT(100 90)'), 50, 'quad_segs=2'),0,0,30));

以上就完成安装了!!
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+centos7下编译安装postgis3 - AIGISSS centos7下编译安装postgis3
本文最后更新于:5 个月前
本文认为已安装PostgreSQL12,安装步骤如 Centos7安装PostgreSQL,最好按照前文先把pg安装好,否则,在postgis,pgrouting安装时,指定pg的安装目录,直接抄路径应该不对,读者要指向自己的安装位置等。
安装前准备
升级cmake
CGAL4-11。因为4-11需要CMake3.11以上
1
2
cmake -version
# cmake version 2.8.12.2
1
2
3
4
5
6
wget https://github.com/Kitware/CMake/releases/download/v3.16.8/cmake-3.16.8.tar.gz
tar -zxvf cmake-3.16.8.tar.gz
cd cmake-3.16.8
./bootstrap --prefix=/usr/local/cmake
gamke
gmake install
postgis
还要安装其他依赖,比如GEOS
,proj
,GDAL
,各版本需要的依赖版本,http://postgis.net/news/ ,如图。

本文基于GEOS 3.8.1,PROJ 6.3.2,GDAL 3.0.4,json-c 0.13.1,CGAL4.14.3,SFCGAL1.3.7,protobuf3.11.4,protobuf-c1.3.3,libxml2-2.9.10,pcre-8.44,PostGIS-3.0.1,pgrouting3.0.2安装。
安装依赖或插件
安装GEOS-3.8.1
1
2
3
4
5
wget https://download.osgeo.org/geos/geos-3.8.1.tar.bz2
tar -jxf geos-3.8.1.tar.bz2
cd geos-3.8.1
./configure --prefix=/usr/local/geos-3.8.1
make && make install
:blush:
安装proj-6.3.2
1
2
3
4
5
6
wget http://download.osgeo.org/proj/proj-6.3.2.tar.gz
tar -zxvf proj-6.3.2.tar.gz
cd proj-6.3.2
./configure --prefix=/usr/local/proj-6.3.2
# 编译时遇到下面的问题,说是sqlite版本太低!编译成功再进行下一步!
make && make install
遇到如图片的问题!

然后我在命令行输入sqlite3的时候,结果是3.22,哪来的3.7.17?

原来是sqlite-devel的版本,但我查询之后是最新的!!于是我删了sqlite-devl,得到的是Checking for module 'sqlite3' No package 'sqlite3' found
暴力升级也没有用,不得不看日志的下半部分。于是搜索关键词PKG_CONFIG_PATH
。找到https://my.oschina.net/zzop/blog/499908这篇文章,其中说到:

:point_right:

cp /usr/local/lib/pkgconfig/sqlite3.pc /usr/lib64/pkgconfig/
重新编译就好了!

安装GDAL
1
2
3
4
5
6
7
8
wget https://download.osgeo.org/gdal/3.0.4/gdal-3.0.4.tar.gz
tar -zxvf gdal-3.0.4.tar.gz
cd gdal-3.0.4
# 带pg的配置,具体需要啥可以./configure --help 查看
./configure --prefix=/usr/local/gdal-3.0.4 --with-pg=yes
make
# 编译时遇到下面的问题,成功则进行下一步!
make install
遇到
1
2
3
collect2: error: ld returned 1 exit status
make[1]: *** [GNUmakefile:82: gdalinfo] Error 1
make: *** [GNUmakefile:112: apps-target] Error 2
解决办法可能是make clean
,紧接着按之前的编译安装!
参考文章:https://stackoverflow.com/questions/60218227/trying-to-install-gdal-3-0-4-on-red-hat-8
我的编译配置:
1
./configure --prefix=/usr/local/gdal-3.0.4 --with-proj=/usr/local/proj-6.3.2 --with-geos=/usr/local/geos-3.8.1/bin/geos-config --with-sqlite3=/usr/local/bin/sqlite3 --with-libjson-c=/usr/local/json-c-0.13.1 --with-pg=yes --with-python=/root/.virtualenvs/aigisss_py/bin/python3.6
安装protubuf
1
2
3
tar -xzvf protobuf-cpp-3.11.4.tar.gz
./configure --prefix=/usr/local/protobuf-3.11.4
make && make install
竟然说bash: ./configure: No such file or directory
解决方案
1
2
yum install automake
autoreconf -i
参考文章:https://stackoverflow.com/questions/24054761/configure-gives-error-in-ubuntu
出现编译错误
1
2
3
make[2]: *** [message.lo] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2
解决方案下载含有all的资源包

参考文章:https://github.com/protocolbuffers/protobuf/issues/6599
安装protobuf-c
1
2
3
4
5
6
wget https://github.com/protobuf-c/protobuf-c/releases/download/v1.3.3/protobuf-c-1.3.3.tar.gz
tar -xzvf protobuf-c-1.3.3.tar.gz
cd protobuf-c-1.3.3
export PKG_CONFIG_PATH=/usr/local/protobuf-3.11.4/lib/pkgconfig
./configure --prefix=/usr/local/protobuf-c-1.3.3
make && make install
安装SFCGAL 1.3.7
由于SFCGAL需要依赖Boost、CGAL、GMP、MPFR这四个软件,所以具体总共需要安装以下四个软件:
boost-devel.x86_64
gmp-devel.x86_64
mpfr-devel.x86_64
CGAL-4.14
为了安装pgrouting3.0.2
需要安装boost1.53
以上,使用yum install boost boost-devel
只能安装版本1.53
, 我使用源码安装的是1.68
1
2
3
4
5
wget https://dl.bintray.com/boostorg/release/1.68.0/source/boost_1_68_0.tar.gz
tar -xzvf boost_1_68_0.tar.gz
cd boost_1_68_0
./bootstrap.sh
./b2 install --with=all
1
2
yum install gmp-devel.x86_64
yum install mpfr-devel.x86_64
安装CGAL
1
2
3
4
5
6
7
wget http://distfiles.macports.org/cgal/cgal-4.14.3.tar.xz
xz -d CGAL-4.14.3.tar.xz
tar -xvf CGAL-4.14.3.tar
cd CGAL-4.14.3
mkdir build && cd build
cmake ..
make && make install
安装pcre
1
2
3
4
5
6
7
wget https://ftp.pcre.org/pub/pcre/pcre-8.44.tar.gz
tar -xzvf pcre-8.44.tar.gz
cd pcre-8.44
./configure --enable-utf8 --prefix=/usr/local/pcre-8.44
make && make intall
echo "/usr/local/pcre/lib" > /etc/ld.so.conf.d/pcre-8.44.conf
ldconfig
安装SFCGAL
遇到以下这个问题:
1
2
c++: internal compiler error: Killed (program cc1plus)
Please submit a full bug report
解决方案:
1
2
3
4
5
6
7
8
sudo dd if=/dev/zero of=/swapfile bs=64M count=16
#count的大小就是增加的swap空间的大小,64M是块大小,所以空间大小是bs*count=1024MB
sudo mkswap /swapfile
#把刚才空间格式化成swap格式
chmod 0600 /swapfile
#该目录权限,不改的话,在下一步启动时会报“swapon: /swapfile: insecure permissions 0644, 0600 suggested.”错误
sudo swapon /swapfile
#使用刚才创建的swap空间
安装编译:
1
2
3
4
5
6
wget https://github.com/Oslandia/SFCGAL/archive/v1.3.7.tar.gz
tar -zxvf SFCGAL-1.3.7.tar.gz
cd SFCGAL-1.3.7
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local/sfcgal-1.3.7 ..
make && make install
装完之后释放空间:
1
2
3
swapoff -a
#详细的用法可以:swapoff --help
#查看当前内存使用情况:free -m
参考文章:https://blog.csdn.net/qq_27148893/article/details/88936044
安装PostGIS
1
./configure --prefix=/usr/local/postgis-3.0.1 --with-gdalconfig=/usr/local/gdal-3.0.4/bin/gdal-config --with-pgconfig=/opt/pg12/bin/pg_config --with-geosconfig=/usr/local/geos-3.8.1/bin/geos-config --with-projdir=/usr/local/proj-6.3.2 --with-xml2config=/usr/local/libxml2-2.9.10/bin/xml2-config --with-jsondir=/usr/local/json-c-0.13.1 --with-protobufdir=/usr/local/protobuf-c-1.3.3 --with-sfcgal=/usr/local/sfcgal-1.3.7/bin/sfcgal-config --with-pcredir=/usr/local/pcre-8.44
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PG_HOME=/opt/pg12
LD_LIBRARY_PATH=$PG_HOME/lib:$LD_LIBRARY_PATH
PATH=$PG_HOME/bin:$PATH
PKG_CONFIG_PATH=$PG_HOME/lib/pkgconfig:$PKG_CONFIG_PATH
export CMAKE_HOME=/usr/bin/cmake
export PROTOBUF_HOME=/usr/local/protobuf-3.11.4
GDAL_HOME=/usr/local/gdal-3.0.4
GDAL_DATA=$GDAL_HOME/share/gdal
LD_LIBRARY_PATH=$GDAL_HOME/lib:/usr/local/lib64:$JRE_HOME/lib:$LD_LIBRARY_PATH
PATH=$GDAL_HOME/bin:$PATH
export PATH=$CMAKE_HOME/bin:$PROTOBUF_HOME/bin:/usr/local/protobuf-c-1.3.3/bin:$PATH
export PKG_CONFIG_PATH LD_LIBRARY_PATH
export LD_LIBRARY_PATH GDAL_DATA
创建postgis扩展
1
2
3
4
5
6
7
su - postgres
psql
create database gistest;
\c gistest
create extension postgis;
#如果安装了sfcgal,创建扩展测试下
create extension postgis_sfcgal;
安装PgRouting
1
2
3
4
5
6
7
8
9
wget https://github.com/pgRouting/pgrouting/releases/download/v3.0.2/pgrouting-3.0.2.tar.gz
tar -zxvf pgrouting-3.0.2.tar.gz
cd pgrouting-3.0.2
mkdir build && cd build
#引入postgres的环境变量
source /home/postgres/.bashrc
cmake ..
make
make install
验证安装:
1
2
3
4
5
su - postgres
psql
create database gistest;
\c gistest
create extension pgrouting;
验证:sql查询
1
SELECT ST_AsX3D(ST_Extrude(ST_Buffer(ST_GeomFromText('POINT(100 90)'), 50, 'quad_segs=2'),0,0,30));

以上就完成安装了!!
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/ab44f639.html b/posts/ab44f639.html
index c5701209..518cd1f4 100644
--- a/posts/ab44f639.html
+++ b/posts/ab44f639.html
@@ -1 +1 @@
-Threejs学习和总结进阶篇 - AIGISSS Threejs学习和总结进阶篇
本文最后更新于:1 年前
相机控制器Controls
相机的基本内容我们已经了解。
正常的项目中,大家的需求都是不一样的,又通常会碰上需求中途改变的情况,我们之前做的简易版相机控制器很难满足此类项目对相机的操作需求。而且,造轮子的前提是当前的框架以及插件已经无法满足自身的需求时,才会考虑造轮子。要不然,项目的进度会被拖得很慢,甚至有可能因此而错过红利期。
好在 Three.js 官方和同道中的朋友们给我们提供了很多相关的插件,我们可以根据需求引入相关的插件来实现需求,本文我们就来看一下官方案例中提供的相机控制器。
从官网下载的代码包里可以发现有很多的相机控制器,文件夹地址为:/examples/js/controls/
,里面的文件插件都是和控制相机和控制模型相关的插件,我们罗列一下相关插件:
- DeviceOrientationControls:陀螺仪相机控制器,实现移动端陀螺仪控制相机。
- DragControls:控制鼠标拖拽移动物体的功能。
- EditorControls:实现相机的旋转、缩放、平移功能,相对于 OrbitControls 的功能差不少,不建议使用。
- FirstPersonControls:第一视角相机控制器。
- FlyControls:飞行相机控制器。
- OrbitControls:轨道控制器。
- OrthographicTrackballControls:正交轨迹球控制器——正交相机使用的轨迹球控制器。
- TrackballControls:轨迹球控制器——透视相机使用的轨迹球控制器。
- PointerLockControls:鼠标锁定相机控制器。
- TransformControls:控制模型位置、缩放、旋转的控制器。
- VRControls:实现 VR 双屏相机控制器。
由于篇幅有限,上面的控制器无法一一介绍,我们将重点介绍三种常用的相机控制器。
OrbitControls
OrbitControls 控制器是我们常用的相机控制器,它的功能丰富,使用简单,为大多数项目的使用插件。
使用操作
使用 OrbitControls 控制器我们可以实现旋转、缩放、平移等功能,下面简单列一下 OrbitControls 控制器的操作方法:
- 围绕焦点旋转:使用鼠标左键拖拽;
- 放大和缩小:使用鼠标中键按住拖拽或者鼠标中键滑动滚轮;
- 平移相机:按住鼠标右键拖拽或者使用键盘的上下左右键。
控制器引入
在项目中使用 OrbitControls 控制器,可分为下面几步。
- 首先,将插件文件引入到项目中:
1
<script src="../js/OrbitControls.js"></script>
- 然后,通过相机和渲染器的 Dom 对象实例化相机:
1
control = new THREE.OrbitControls(camera, renderer.domElement);
- 最后,在每一帧渲染里面更新相机的位置:
1
2
3
4
function render() {
control.update();
renderer.render(scene, camera);
}
这样,我们就完成了一个最简单的 OrbitControls 控制器使用。
属性和方法
OrbitControls 控制器最大的优势就是有丰富的配置项,供我们修改来实现项目中的需求,接下来我们看看有哪些属性配置。
属性 描述 enabled 是否开启当前控制器,默认值是 True,如果设置为 False,将无法通过操作修改相机。 target 控制器的焦点位置,是一个 THREE.Vector3
对象,默认是 (0, 0, 0)
minDistance 相机距离焦点的最近距离,默认值是0。 此属性适用于透视相机 PerspectiveCamera。 maxDistance 相机距离焦点的最远距离,默认值是 Infinity(无限远), 此属性适用于透视相机 PerspectiveCamera。 minZoom 相机距离焦点的最近距离,默认值是0,此属性适用于正交相机 OrthographicCamera。 maxZoom 相机距离焦点的最远距离,默认值是 Infinity(无限远),此属性适用于正交相机 OrthographicCamera。 minPolarAngle 相机位置和焦点与焦点和最上方组成的最小夹角限制,默认值是0。 maxPolarAngle 相机位置和焦点与焦点和最上方组成的最大夹角限制,默认值是 Math.PI,也就是180度角。 minAzimuthAngle 当前相机沿水平方向顺时针旋转的弧度,默认值是 - Infinity
。 maxAzimuthAngle 当前相机沿水平方向逆时针旋转的弧度,默认值是 Infinity
。 enableDamping 是否开启拖拽惯性移动,即拖拽停止相机会有缓慢停止的距离移动,默认值是 False。 dampingFactor 拖拽惯性移动的阻力,默认值是 0.25。 enableZoom 是否开启缩放操作,默认值是 True。 zoomSpeed 缩放速度,默认值是 1.0。 enableRotate 是否开启相机绕焦点旋转操作,默认值是 True。 rotateSpeed 旋转速度,默认值是 1.0。 enablePan 是否开启相机平移操作,默认值是 True。 panSpeed 平移的速度,默认值是 1.0。 screenSpacePanning 修改相机平移的方向,默认值是 False,即沿 x 轴正负方向和 y 轴正负方向移动。可选值是 True,可以修改为沿 x 轴正负方向和 y 轴正负方向移动。 keyPanSpeed 键盘上下左右键移动相机的速度,默认值是 7.0。 autoRotate 当前相机是否自动旋转,默认值是 False,不自动旋转。 autoRotateSpeed 自动旋转的速度,默认值是 2.0,即渲染满60帧的情况下30秒旋转360度。 enableKeys 是否开启键盘控制先机平移,默认值是 True。
OrbitControls 控制器的属性配置介绍完了,我们看看 OrbitControls 控制器还有那些方法。
update()
OrbitControls 控制器更新相机的方法,需要在每一帧里面调用。
reset()
重置方法,相机回到初始位置。
dispose()
销毁当前实例化的 OrbitControls 控制器。
change 回调
我们还可以监听相机改变回调,如果控制器修改了相机,将会产生一个回调:
1
2
3
controls.addEventListener('change', function(){
console.log("相机动了!");
});
最后,我附上 OrbitControls 控制器案例:
也可以从这里获取案例源码:
TrackballControls
TrackballControls 控制器比 OrbitControls 控制器更自由,TrackballControls 控制器能够沿焦点进行球形旋转,没有死角,但比 OrbitControls 控制器少一些相关的功能配置。如何选择使用它们还是看项目需求,接下来还是先看如何操作。
注意,透视相机和正交相机使用的不是一个插件,此插件为透视相机使用,如果是正交相机请使用 OrthographicTrackballControls。
使用操作
使用 TrackballControls 控制器我们可以实现旋转、缩放、平移等功能,下面说一下如何使用 TrackballControls 控制器进行操作:
- 围绕焦点旋转:使用鼠标左键拖拽;
- 放大和缩小:使用鼠标中键按住拖拽或者鼠标中键滑动滚轮;
- 平移相机:按住鼠标右键拖拽或者使用键盘的上下左右键。
TrackballControls 控制器和 OrbitControls 控制器的操作相同,没什么可说的。
控制器引入
在项目中使用 TrackballControls 控制器和 OrbitControls 控制器的方法雷同,分为下面几步。
- 首先,将插件文件引入到项目中:
1
<script src="../js/TrackballControls.js"></script>
- 然后,通过相机和渲染器的 Dom 对象实例化相机:
1
control = new THREE.TrackballControls(camera, renderer.domElement);
- 最后,在每一帧渲染里面更新相机的位置:
1
2
3
4
function render() {
control.update();
renderer.render(scene, camera);
}
属性和方法
属性 描述 enabled 是否开启当前控制器,默认值是 True,如果设置为 False,将无法通过操作修改相机。 rotateSpeed 控制相机旋转速度,默认值是 3.0。 zoomSpeed 控制相机缩放速度,默认值是 1.2。 panSpeed 控制相机平移速度,默认值是 0.3。 noRotate 关闭相机旋转,默认 False,开启。 noZoom 关闭相机缩放,默认 False,开启。 noPan 关闭相机移动,默认 False 开启。 staticMoving 关闭拖拽惯性移动 默认值 False,开启。 dynamicDampingFactor 拖拽惯性移动阻力,默认值是 0.2。 minDistance 相机距离焦点的最近距离,默认值是 0。 maxDistance 相机距离焦点的最远距离,默认值是 Infinity(无限远)。
相对于 OrbitControls 控制器,TrackballControls 控制器的属性少一些,但是相关的功能还是比较全面的。TrackballControls 控制器的方法也和 OrbitControls 控制器的方法雷同。
update()
TrackballControls 控制器更新相机的方法,需要在每一帧里面调用。
reset()
重置方法,相机回到初始位置。
dispose()
销毁当前实例化的 TrackballControls 控制器。
change 回调
我们还可以监听相机改变回调,如果控制器修改了相机,将会产生一个回调:
1
2
3
controls.addEventListener('change', function(){
console.log("相机动了!");
});
最后,附上 TrackballControls 控制器案例:
也可以从这里获取案例源码:
DeviceOrientationControls
最后,我们介绍的这个控制器只兼容含有陀螺仪的移动端。DeviceOrientationControls 控制器可以通过获取设备的陀螺仪状态来控制相机的朝向。
如果你还对陀螺仪不了解,请点击查看这里,在这里不多说。
DeviceOrientationControls 的内容配置较少,我们先看一下案例。
使用手机打开网址:点击这里 ,然后手机朝下然后移动,你会发现能够通过手机的转向来控制相机的朝向,是不是很神奇。接下来我们看看如何引入到项目中。
- 首先,将插件文件引入到项目中:
1
<script src="../js/DeviceOrientationControls.js"></script>
- 然后,通过相机对象实例化相机:
1
control = new THREE.DeviceOrientationControls(camera);
- 最后,在每一帧渲染里面更新相机的位置:
1
2
3
4
function render() {
control.update();
renderer.render(scene, camera);
}
这样我们就完成了对 DeviceOrientationControls 控制器的添加。
DeviceOrientationControls 控制器相关的配置也很少,只有一个 Enabled 属性,设置为 True,则控制器会更新相机的位置,反之,设置 False 将无法更新相机位置。
还有一个方法就是销毁当前控制器的方法:
1
controls.dispose(); //销毁当前控制器
最后,附上源码:
模型加载Loaders
现在市面上的 3D 模型有上百种,每一种格式都有不同的用途,不同的功能和复杂程度。尽管 Three.js 提供了很多的加载器,但选择正确的格式和工作流程将为以后的工作节省大量时间和成本。而且某些格式难以使用,效率低下,甚至有些目前还未完全被支持。
推荐使用的模型格式
官方推荐我们使用的 3D 模型的格式为 glTF,由于 glTF 专注于传输,因此它的传输和解析的速度都很快。glTF 模型的功能包括网格、材质、纹理、蒙皮、骨骼、变形动画、骨骼动画、灯光以及相机。
如果当前的首选不是 glTF 格式,那么推荐使用 Three.js 定期维护并且流行的格式 FBX、OBJ 或者 COLLADA 格式,Three.js 也有自己独有的 JSON 格式。我们接下来将介绍这五种格式。
Three.js 的 JSON 格式
这里的 JSON 格式指的是 Three.js 可以将其转换为场景 3D 对象的 JSON 格式模型。这种格式内部一般必有的四项为:
- metadata:当前模型的相关信息以及生成的工具信息;
- geometries:存储当前模型所使用的几何体的数组;
- materials:存储当前模型所使用的材质的数组;
- object:当前模型的结构以及标示所应用到的材质和几何体标示。
所有的模型网格,几何体和材质都有一个固定的 UUID 标识符,在 JSON 格式中均通过 UUID 引用。
3D 对象转成 JSON
所有的 THREE.Object3D
对象都可以转成 JSON 字符串保存成为文件,但我们不能直接将对象转成 JSON,因为 JSON 无法保存函数。Three.js 给我们提供了一个 toJSON() 的方法来让我们将其转换为可存储的 JSON 格式。
1
2
3
4
var obj = scene.toJSON(); //将整个场景的内容转换成为 JSON 对象
var obj = group.toJSON(); //将一个模型组转成 JSON 对象
var obj = mesh.toJSON(); //将一个模型网格转成 JSON 对象
var JSONStr = JSON.stringify(obj); //将 JSON 对象转换成 JSON 字符串
按照这种方式,我们就可以将生成的场景模型保存为文件。
使用 ObjectLoader 加载 JSON 模型
这里我们将使用到 Three.js 内置的对象 THREE.ObjectLoader
加载模型。
直接加载 Three.js 生成的 JSON 对象,代码如下:
1
2
3
4
var obj = scene.toJSON(); //将整个场景的内容转换成为json对象
let loader = new THREE.ObjectLoader(); //实例化ObjectLoader对象
let scene = loader.parse(obj); //将json对象再转换成3D对象
加载外部的 JSON 文件:
1
2
3
4
5
6
let loader = new THREE.ObjectLoader(); //实例化ObjectLoader对象
//加载模型,并在回调中将生成的模型对象添加到场景中
loader.load("../js/models/json/file.json", function (group) {
scene.add(group);
});
案例地址:请点击这里。
案例的右上角有四个点击事件:
- 添加模型:将在场景内随机生成一组立方体,每次都不相同。
- 导出模型:将场景内这一组立方体导出到本地 JSON 文件。
- 导入模型:可以选择将符合 JSON 文件作解析并导入到场景内。
- 加载模型:将加载服务器上面的一个 JSON 文件。
案例代码地址:请点击这里。
glTF 格式文件导入
glTF 格式的 3D 格式文件是官方推荐的使用格式,这种格式的文件我们可以在 Sketchfab 官网下载,这是一个国外比较知名的模型网站。
- 下载地址:点击这里。
我们可以在这里下载一些免费的 glTF 格式的模型。
我在官网上找了个不错的模型做了一个案例,点击这里查看。

模型加载的速度会有些慢,大家可以等待下便能够看到这个小汽车。
接下来我们便讲解一下加载 glTF 模型的流程。
- 首先,将 GLTFLoader 加载器插件引入到页面,插件在官方包的
/examples/js/loaders/
文件夹中,一些文件的导入插件都在这个文件夹内,大家有兴趣可以研究一下:
1
<script src="../js/loaders/GLTFLoader.js"></script>
- 然后创建一个加载器:
1
var loader = new THREE.GLTFLoader();
- 使用加载器加载模型,并调节一下模型大小在场景内展示:
1
2
3
4
loader.load('../js/models/gltf/scene.gltf', function (gltf) {
gltf.scene.scale.set(.1,.1,.1);
scene.add(gltf.scene);
});
有时候我们可能不明白,我加载了一个模型,哪一部分是需要导入场景的模型呢?
这里我们可以先将解析的出来的模型对象打印一下,然后通过查看对象属性来了解导入场景内的对象,就比如 glTF 模型转换出来的对象的 scene 属性就是需要导入场景的对象,而 JSON 格式的模型是直接可以导入的对象。
模型加载案例源码:请点击这里。
FBX 模型导入
FBX 最大的用途是,在诸如 Max、Maya、Softimage 等软件间进行模型、材质、动作和摄影机信息的互导,这样就可以发挥 Max 和 Maya 等软件的优势。可以说,FBX 是最好的互导方案。

接下来我们看一个 FBX 模型导入的案例,是我在网上下载的一个 FBX 格式的模型,导入到场景内的效果:请点击这里查看。
接下来,我们看下它的实现过程。
首先我们需要导入 FBXLoader 插件,并且还需要额外增加一个解析二进制文件的插件 inflate.min.js
,不导入该文件的话,除了一些字符串存储的 FBX 格式,别的格式都会报错:
1
2
<script src="../js/loaders/inflate.min.js"></script>
<script src="../js/loaders/FBXLoader.js"></script>
创建 FBX 加载器:
1
var loader = new THREE.FBXLoader();
修改模型大小,并设置每个模型网格可以投射阴影:
1
2
3
4
5
6
7
8
9
10
loader.load('../js/models/fbx/file.fbx', function (fbx) {
fbx.scale.set(.1,.1,.1);
fbx.traverse(function (item) {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
scene.add(fbx);
});
这样就实现了 FBX 模型的导入。
案例源码地址:请点击这里。
OBJ 格式模型导入
OBJ 文件是 3D 模型文件格式。由 Alias|Wavefront 公司为 3D 建模和动画软件 Advanced Visualizer 开发的一种标准,适合用于 3D 软件模型之间的互导,也可以通过 Maya 读写。
OBJ 文件是一种文本文件,可以直接用写字板打开进行查看和编辑修改,但不包含动画、材质特性、贴图路径、动力学、粒子等信息。
OBJ 文件的导出通常会和 MTL 格式一同导出,MTL 作为 OBJ 文件的附属文件,却有着 OBJ 文件需要的贴图材质,所以,我们通常使用时,将它们两个文件一同导入。

这是我使用官网提供的一个模型制作的一个案例,查看地址:请点击这里。
我们看下实现导入的过程。
首先,我们需要将 OBJLoader 插件和 MTLLoader 插件引入页面:
1
2
<script src="../js/loaders/OBJLoader.js"></script>
<script src="../js/loaders/MTLLoader.js"></script>
实例化 MTLLoader :
1
2
3
4
//创建MTL加载器
var mtlLoader = new THREE.MTLLoader();
//设置文件路径
mtlLoader.setPath('../js/models/obj/');
如果有需要,我们还可以设置纹理文件夹地址:
1
2
//设置纹理文件路径
mtlLoader.setTexturePath('../js/models/obj/');
加载 MTL 文件,并在文件加载成功后,创建 OBJLoader 并设置对象应用当前的材质:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//加载mtl文件
mtlLoader.load('female02.mtl', function (material) {
//创建OBJ加载器
var objLoader = new THREE.OBJLoader();
//设置当前加载的纹理
objLoader.setMaterials(material);
objLoader.setPath('../js/models/obj/');
objLoader.load('female02.obj', function (object) {
//添加阴影
object.traverse(function (item) {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
//缩放
object.scale.set(.3,.3,.3);
scene.add(object);
})
});
我们再去加载 OBJ 文件,加载成功的文件就是可以导入到场景内的 3D 对象。
案例源码查看地址:请点击这里。
COLLADA 模型导入
COLLADA 是一个开放的标准,最初用于 3D 软件数据交换,由 SCEA 发起,现在则被许多著名厂家(如 Autodesk、XSI 等)支持。COLLADA 不仅仅可以用于建模工具之间的数据交换,也可以作为场景描述语言用于小规模的实时渲染。
COLLADA DOM 拥有丰富的内容用于表现场景中的各种元素,从多边形几何体到摄像机无所不包。我们可以通过 COLLADA DOM 库来进行场景文件的读取与处理操作。

上面是我写的一个模型导入案例,案例地址:请点击这里。
我们看下实现步骤。
首先引入 ColladaLoader 插件:
1
<script src="../js/loaders/ColladaLoader.js"></script>
接着实例化 ColladaLoader 对象:
1
var loader = new THREE.ColladaLoader();
最后加载文件并调整文件大小,添加到场景内:
1
2
3
4
5
6
7
8
9
10
11
12
13
loader.load('../js/models/collada/elf.dae', function (collada) {
//添加阴影
collada.scene.traverse(function (item) {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
//缩放
collada.scene.scale.set(5,5,5);
scene.add(collada.scene);
});
案例源码查看:请点击这里。
注意事项
1. 如何知道,加载完成的模型需要将哪部分导入到场景?
一般情况下都是将自身导入,比如 FBX,OBJ,JSON 等,还有一种,会在里面生成一个可导入 scene 属性,如 glTF 和 COLLADA 文件。如果导入哪部分你无法确定,你可以把模型对象打印到控制台查看,然后尝试往场景内导入。
2.导入到场景内的模型无法查看,而且也没有报错,为什么?
这种情况可能由多种情况造成的,一般主要有下面两种情况:
- 模型太小或者太大,这种情况可以尝试放大一千倍或者缩小一千倍来查看效果。
- 模型的位置太偏,根本不在相机照射范围内,这种问题我们可以将模型居中到相机照射的焦点位置查看,如何居中我们将在后面的课中讲解。
Three.js 动画
动画一般可以分为两种:一种是变形动画,另一种是骨骼动画。下面,我们先介绍一下变形动画。
变形动画
变形动画,通过修改当前模型的顶点位置来实现。比如,一个动画需要变动十次才可以实现,那么我们需要为当前模型的每一个顶点定义每一次所在的位置,Three.js 通过这一次次的修改实现了动画的整个流程。
为了帮助大家更好地理解变形动画的实现与使用,我创建了一个案例,查看地址为:点击这里。
在这个案例的右上角,我们能发现两个可切换的拖拽条。这两个拖拽条对应的是两个变形目标数组,拖拽范围是0-1,即当前的变形目标对本体的影响程度。拖拽它们,可发现界面中的立方体也会跟随之变动,从而影响当前的立方体。接下来我讲解一下,该案例的实现过程。
首先,创建模型的几何体,并为几何体 morphTargets 赋值两个变形目标。morphTargets 是一个数组,我们可以为其增加多个变形目标。在给 morphTargets 添加变形目标时,需要为其定义一个名称和相关的顶点,这个顶点数据必须和默认的模型的顶点数据保持一致,设置完后,我们需要调用 geometry 的 computeMorphNormals()
进行更新,代码如下:
1
2
3
4
5
6
7
8
9
10
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
// 创建两个影响立方体的变形目标
var cubeTarget1 = new THREE.BoxGeometry(2, 10, 2);
var cubeTarget2 = new THREE.BoxGeometry(8, 2, 8);
// 将两个geometry的顶点放入到立方体的morphTargets里面
cubeGeometry.morphTargets[0] = {name: 'target1', vertices: cubeTarget1.vertices};
cubeGeometry.morphTargets[1] = {name: 'target2', vertices: cubeTarget2.vertices};
cubeGeometry.computeMorphNormals();
然后,为当前模型设置材质,变形目标作为参数之一,可以使其变形。
1
var cubeMaterial = new THREE.MeshLambertMaterial({morphTargets: true, color: 0x00ffff});
接着,将创建好的网格模型添加到场景中。这时可以在 mesh 对象中找到 morphTargetInfluences 配置项,它也是一个数组,和 geometry 的 morphTargets 相对应,主要用来设置当前变形目标对本体的影响度,默认值为0-1,0为不影响本体,1为完全影响本体:
1
2
3
4
5
6
7
8
gui = {
influence1:0.01,
influence2:0.01,
update : function () {
cube.morphTargetInfluences[0] = gui.influence1;
cube.morphTargetInfluences[1] = gui.influence2;
}
};
至此,我们就手动实现了一个变形动画。在这个过程中,我们发现,变形动画是由于不断修改变形目标对本体的影响度而产生的。我们可以通过这个原理实现其他变形动画。
案例代码查看地址:请点击这里。
骨骼动画
实现骨骼动画,我们需要生成一个与模型相关的骨架。骨架中的骨骼与骨骼之间存在关联,模型的每一个要动的顶点需要设置影响它的骨骼以及骨骼对顶点的影响度。
和变形动画相比,骨骼动画更复杂一些,但又有更多的灵活性。使用变形动画,我们需要把所有的每一次的变动都存在一个顶点数组中,而骨骼动画,只需要设置骨骼的相关信息,就可以实现更多的动画。
下面我们看一个骨骼动画的简单案例:点击这里

这是官方提供的一个案例。我对其做了些简单修改,以显示出当前一个柱形图形的骨骼。实现起来比较复杂,我们需要先理解它是怎么实现的。
首先, 我们创建了一个圆柱几何体,通过圆柱的几何体每个顶点的 y 轴坐标位置来设置绑定的骨骼的下标和影响的程度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//遍历几何体所有的顶点
for (var i = 0; i < geometry.vertices.length; i++) {
//根据顶点的位置计算出骨骼影响下标和权重
var vertex = geometry.vertices[i];
var y = (vertex.y + sizing.halfHeight);
var skinIndex = Math.floor(y / sizing.segmentHeight);
var skinWeight = (y % sizing.segmentHeight) / sizing.segmentHeight;
geometry.skinIndices.push(new THREE.Vector4(skinIndex, skinIndex + 1, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - skinWeight, skinWeight, 0, 0));
}
几何体的 skinIndices 属性和 skinWeights 属性分别用来设置绑定的骨骼下标和权重(骨骼影响程度)。
相应的,我们需要一组相关联的骨骼。骨骼具有嵌套关系,才得以实现一个骨架。圆柱体比较简单,我们直接创建一条骨骼垂直嵌套的骨骼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bones = [];
var prevBone = new THREE.Bone();
bones.push(prevBone);
prevBone.position.y = -sizing.halfHeight;
for (var i = 0; i < sizing.segmentCount; i++) {
var bone = new THREE.Bone();
bone.position.y = sizing.segmentHeight;
bones.push(bone); //添加到骨骼数组
prevBone.add(bone); //上一个骨骼定义为父级
prevBone = bone;
}
创建纹理时,我们还需要设置当前材质属性,并开启骨骼动画对其的修改权限,将材质的 skinning 属性设置为 true:
1
2
3
4
var lineMaterial = new THREE.MeshBasicMaterial({
skinning: true,
wireframe: true
});
最后,我们需要创建骨骼材质,并将模型绑定骨骼:
1
2
3
4
mesh = new THREE.SkinnedMesh(geometry, [material, lineMaterial]);
var skeleton = new THREE.Skeleton(bones); //创建骨架
mesh.add(bones[0]); //将骨骼添加到模型里面
mesh.bind(skeleton); //模型绑定骨架
这样,我们就使用 Three.js 创建了一个简单的骨骼动画。使用 dat.gui
,便于我们修改每一个骨骼的 poisition、rotation 和 scale 并查看对当前模型的影响。
案例的源码地址:点击这里。
两种动画的区别
变形动画主要用于精度要求高的动画,比如人物的面部表情。其优点是动画的展现效果很到位,缺点就是扩展性不强,只能执行设置好的相关动画。
骨骼动画主要用于精度要求相对低一些,但需要丰富多样的动画的场合,就比如人物的走动,攻击防御等动画,我们可以通过一套骨骼,修改相应骨骼的位置信息直接实现相应的效果。它没有变形动画的精度高,但可以实现多种多样的效果。
总结: 我们可以根据项目的需求来设置不同的动画,就比如一个人物模型,说话我们使用变形动画去实现,而肢体动作使用骨骼动画去实现。
导入模型动画
在 Three.js 动画系统中,你可以为模型的各种属性设置动画,如骨骼动画,变形动画,以及材质的相关属性(颜色,透明度, 是否可见)。动画属性可以设置淡入淡出效果以及各种扭曲特效,也可以单独改变一个或多个对象上的动画影响程度和动画时间。
为了实现这些,Three.js 动画系统在2015年修改为了类似于 Unity 和虚幻引擎4的架构。接下来我们了解下这套动画系统的主要组件以及它们是如何协同工作的。
动画片段(Animation Clips)
在我们成功导入模型以后,如果模型拥有相关的动画属性,会在返回的模型数据中产生一个名为 animations 的数组,数组的每一个子项都是一个 AnimationClips 对象。
每一个单独 AnimationClips 对象相应的保存着模型的一个动画的数据,假如,如果模型网格是一个人物角色,第一个 AnimationClips 对象有可能保存的是人物走动的动画,第二个 AnimationClips 对象用于跳跃,第三个用于攻击动画等等。
关键帧轨迹(Keyframe Tracks)
在 AnimationClips 对象内部,一般有四个属性:
- name:当前动画的名称;
- uuid:一个不会重复的 uuid;
- duration:当前动画一个循环所需要的时间;
- tracks:轨迹,即当前动画每一次切换动作所需要的数据。
假设当前的动画是骨骼动画,在关键帧轨迹中存储的数据是每一帧骨骼随着时间变动的数据(位置,旋转和缩放等)。
如果当前动画是一个变形动画,在关键帧轨迹中将会把顶点数据的变动存储在其中(比如实现人脸的笑以及哭等动作)。
动画混合器(Animation Mixer)
在动画片段中存储的数据仅仅构成了动画实现的基础,实际的播放权力在动画混合器的手中。你可以想象动画混合器其实不仅仅只是作为动画的播放器,它还可以同时控制几个动画,混合它们或者合并它们。
动画播放器(Animation Actions)
这个英文我更乐意将它翻译成动画播放器,因为我们最终需要将数据生成一个动画播放器来操作当前的动画执行,暂停或者停止,是否使用淡入淡出效果或者将动画加快或减慢。
动画对象组(Animation Object Groups)
如果你希望一组模型对象共享当前的动画,我们可以使用动画对象组来实现。
通过导入模型显示动画

变形动画
我们首先查看一个官方的模型案例,这个案例是一匹马奔跑的动画,我们也可以通过下面地址查看:点击这里。
接下来我们看一下这匹马是如何实现的。
- 在模型加载成功以后,我们首先将模型创建出来,并将材质的 morphTargets 设置为 ture,使顶点数据信息可以受变形动画影响:
1
2
3
4
5
6
7
mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({
vertexColors: THREE.FaceColors,
morphTargets: true
}));
mesh.castShadow = true;
mesh.scale.set(0.1, 0.1, 0.1);
scene.add(mesh);
- 然后我们创建了一个针对于该模型的混合器:
1
mixer = new THREE.AnimationMixer(mesh);
- 接着使用变形目标数据创建一个动画片段:
1
var clip = THREE.AnimationClip.CreateFromMorphTargetSequence('gallop', geometry.morphTargets, 30);
- 使用混合器和动画片段创建一个动画播放器来播放:
1
2
3
var action = mixer.clipAction(clip); //创建动画播放器
action.setDuration(1); //设置当前动画一秒为一个周期
action.play(); //设置当前动画播放
- 最后,我们还需要在重新绘制循环中更新混合器,进行动作更新:
1
2
3
4
5
6
7
8
9
10
11
12
function render() {
control.update();
var time = clock.getDelta();
//由于模型导入是异步的,所以我们再模型没有加载完之前是获取不到混合器的
if (mixer) {
mixer.update(time);
}
renderer.render(scene, camera);
}

骨骼动画
骨骼动画模型,我们使用的是 gltf 格式,这个模型是在 Sketchfab 网站下载的,案例是一个小姐姐跳舞的片段,查看地址:点击这里。
gltf 格式的模型导入进来后,我们可以直接通过 animations 数组创建播放器:
1
2
mixer = new THREE.AnimationMixer(obj); //通过当前模型创建混合器
action = mixer.clipAction(gltf.animations[0]); //通过动画数据创建播放器
直接调用播放器的播放事件让动画播放:
1
action.play();
最后,我们还需要在循环渲染中更新混合器,并将每一帧渲染的间隔时间传入:
1
2
3
4
5
6
7
8
function render() {
control.update();
var time = clock.getDelta();
if (mixer) {
mixer.update(time);
}
renderer.render(scene, camera);
}
Tween.js 补间动画
Tween 是什么
Tween.js 是 JavaScript 中一个简单的补间动画库,包含各种经典动画算法。Tween.js 支持数字对象的属性和 CSS 样式属性赋值,API 简单且强大,支持链式调用。
补间(动画)(来自 In-Between)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这个过程需要多长时间,补间引擎将负责计算从起始点到结束点的值。
在 Three.js 中,我们有一些修改模型位置,旋转和缩放的需求,却无法直接在 WebGL 中使用 CSS3 动画来实现,而 Tween.js 恰好给我们提供了一个很好的解决方案。
比如我们要实现一个模型从 A 点到 B 点的位置移动,常规的实现方法,是使用 setInterval、requestAnimationFrame 手动计算出特定时间的位置点,很不易于管理与查看。而 Tween.js 可以自动根据起始点位置和动画时长计算出所有的位置点,可以很方便地对其进行获取和管理。
简单应用
接下来,我们通过一个案例,带大家了解如何在 Three.js 应用中使用 Tween.js。
案例 Demo 查看地址:点击这里。
案例代码查看地址:点击这里。
本案例的开发思路是:首先获取目标模型的初始位置,然后实例化 Tween,接着设置目标位置,启动 Tween,在 TWEEN.onUpdate() 回调中改变目标模型的位置,从而实现目标模型从初始位置平滑移动到目标位置的动画。
实现代码如下:
1
2
3
4
5
6
7
8
9
//设置tween
var position = {x:-40, y:0, z:-30};
tween = new TWEEN.Tween(position);
//设置移动的目标和移动时间
tween.to({x:40, y:30, z:30}, 2000);
//设置每次更新的回调,然后修改几何体的位置
tween.onUpdate(function (pos) {
cube.position.set(pos.x, pos.y, pos.z);
});
上面代码,首先创建一个 position 对象,存储了当前立方体的位置数据。然后,通过当前的对象创建了一个补间 tween。紧接着,设置每一个属性的目标位置,并告诉 Tween 在 2000 毫秒(动画时长)内移动到目标位置。最后,设置 Tween 对象每次更新的回调,即在每次数据更新以后,将立方体位置更新。
Tween 对象不会直接执行,需要我们调用 start()
方法激活,即 tween.start()
。
1
2
3
4
5
6
//声明一个保存需求修改的数据对象。
gui = {
start:function () {
tween.start();
}
};
想要完成整个过程,我们还需要在每帧里面调用 TWEEN.update
,来触发 Tween 对象更新位置:
1
2
3
4
5
6
7
8
9
function render() {
//更新Tween
TWEEN.update();
control.update();
renderer.render(scene, camera);
}
链式调用
链式调用可以简化大量代码,逻辑清晰集中,便于查看和修改。
Tween 插件也支持链式调用的方法,并且还会修改实例化时传入的对象,如下代码:
1
2
3
4
5
6
7
8
9
10
11
//设置tween
var position = {x:-40, y:0, z:-30};
tween = new TWEEN.Tween(position);
//设置移动的目标和移动时间
tween.to({x:40, y:30, z:30}, 2000);
//设置每次更新的回调,然后修改几何体的位置
tween.onUpdate(function (pos) {
cube.position.set(pos.x, pos.y, pos.z);
});
可以简化为链式调用:
1
2
//直接链式实现tween
tween = new TWEEN.Tween(cube.position).to({x:40, y:30, z:30}, 2000);
Tween 对象方法
控制动画方法
Tween 对象控制动画的方法主要包括开始、取消、重复等方法。
.start()
如果你想激活一个补间,请使用这个方法,调用方式如下所示:
1
tween.start();
start()
方法还接受一个时间参数,添加该参数后,补间不会立即被激活,Tween 动画将在延时该时间数后才开始动画。否则它将立刻开始动画,且在第一次调用 TWEEN.update
时开始计时。如果设置的时间已经小于计时的总时间,那计算出来的位置数据将是参数设置时间开始后,运行到的所在位置。
.stop()
这个方法刚好和 start()
方法对应,如果你想取消一个补间,直接调用这个方法即可:
1
tween.stop();
.update()
其实每个补间都有一个更新方法,只不过我们多会使用 TWEEN.update
,而不会单独调用它(见下方全局函数)。
.chain()
当你按顺序排列不同的补间时,例如当上一个补间结束时立即启动另外一个补间,我们称它为链式补间。关于链式补间的案例请见下面两个链接。
案例 Demo 查看地址:点击这里
案例代码查看地址:点击这里
调用方法为:
1
tweenA.chain(tweenB);
或者,采用一个无限的链式,即 tweenA 与 tweenB 无限循环,便可以写成:
1
2
tweenA.chain(tweenB);
tweenB.chain(tweenA);
在其他情况下,您可能需要将多个补间链接到另一个补间,以使它们(链接的补间)同时开始动画:
1
tweenA.chain(tweenB,tweenC);
警告:调用 tweenA.chain(tweenB)
实际上修改的是 tweenA,tweenB 总在 tweenA 完成时启动。chain()
的返回值只是 tweenA,不是一个新的 Tween。
链接多个补间时,比如 tweenA.chain(tweenB, tweenC)
表示 tweenA 动画结束后,tweenB 和 tweenC 动画同时开始,如果因 tweenB 和 tweenC 修改的属性相同,而存在冲突时,经测试写在后面的属性将是最终动画位置。
注意,一般不要让两个同时开始的补间存在属性冲突。
.repeat()
如果想让一个补间永远重复,可以无限链接自己,但更好的方法是使用 repeat()
方法。它接受一个参数,描述第一个补间完成后需要重复多少次,如下代码示例:
1
2
tween.repeat(10); // 循环10次
tween.repeat(Infinity); // 无限循环
我们可以将案例中 simple.html
文件里面的调用改成无限循环,代码如下:
1
tween = new TWEEN.Tween(cube.position).to({x:40, y:30, z:30}, 2000).repeat(Infinity);
.yoyo()
该方法只有在补间使用 repeat()
方法时才会被调用。我们调用 yoyo()
以后,位置的切换就变成了从头到尾,从尾到头这样的循环过程。
单个补间无限从头到尾的循环,可以写成这样:
1
tween = new TWEEN.Tween(cube.position).to({x:40, y:30, z:30}, 2000).repeat(Infinity).yoyo(true);
进一步了解 yoyo()
方法的使用,可查看下面案例。
案例 Demo 查看地址:点击这里。
案例代码查看地址:点击这里。
.delay()
这个方法用于控制激活前的延时,即触发 start()
事件后,需要延时到设置的 delay 时间,才会真正激活,使用方法见下面代码所示:
1
2
tween.delay(1000);
tween.start();
回调函数
Tween 每次的位置更新后,都会触发 onUpdate 回调函数,我们可以在此回调中修改模型位置。
在之前案例的 simple.html
中,我们在每次更新回调中获取更新后的位置信息并修改模型几何体的位置:
1
2
3
4
//设置每次更新的回调,然后修改几何体的位置
tween.onUpdate(function (pos) {
cube.position.set(pos.x, pos.y, pos.z);
});
目前,补间支持的回调函数主要有以下几种。
- onStart
在补间计算开始前的回调,每个补间只能触发一下,即使使用 repeat()
方法循环,这个回调也只被触发一次。
- onStop
通过调用 stop()
方法停止的补间会触发当前回调,如果是正常完成的补间将不会触发此回调。
- onUpdate
每次补间更新后,我们可以在此回调中获取更新后的值。
- onComplete
当补间正常完成时,将会触发此回调。通过使用 stop()
停止的补间将不会触发此回调。
总结
在 Three.js 中使用 Tween.js,能够很方便地改变模型的位置,不需要手动计算,便能获取到具体的位置数据,以实现我们的需求。
Three.js 场景交互
浏览器是一个 2D 视口,而 Three.js 展示的是 3D 场景。场景交互时,需要在二维平面中控制三维场景的模型,那如何将 2D 视口的 x 和 y 坐标转换成 Three.js 场景中的 3D 坐标呢?
好在 Three.js 已经有了解决相关问题的方案,那就是 THREE.Raycaster
射线,用于鼠标拾取(计算出鼠标移过的三维空间中的对象)等。我们看下面这张图片:

我们一般都会设置三维场景的显示区域,如果指明当前显示的 2D 坐标给 THREE.Raycaster
,它将生成一条从显示起点到终点的射线,也就是射线与近视面交点和射线与远视面交点连成的这一条直线。在相机视角下查看,只是一个点,射线会穿过整个显示场景,并按从近到远的顺序返回与模型相交的数据。
THREE.Raycaster 构造函数和对象方法
实例化
1
new THREE.Raycaster( origin, direction, near, far );
该实例化函数 Raycaster 包含了四个参数。
- origin:光线投射的原点矢量;
- direction:光线投射的方向矢量,应该是被归一化的;
- near:投射近点,用来限定返回比 near 要远的结果。near 不能为负数,缺省为 0;
- far:投射远点,用来限定返回比 far 要近的结果。far 不能比 near 小,缺省为无穷大。
属性
THREE.Raycaster
的属性可以在实例化对象后有修改需求时再修改。除了上面提到的 origin、direction、near、far 四个属性外,我们还有可能用到另一个属性:
- linePrecision:射线和线相交的精度,浮点数类型的值。
方法
THREE.Raycaster
给我们提供了一系列的方法,比如修改射线的位置,判断与某些模型是否相交等,接下来我们列举一些经常使用的方法。
.set()
:此方法可以重新设置射线的原点和方向,从而更新射线位置。
1
.set(origin,direction)
其中,参数 origin 用来设置射线新的原点矢量位置,direction 用来设置基于原点位置的射线的方向矢量。
.setFromCamera()
:使用当前相机和界面的 2D 坐标设置射线的位置和方向。
1
.setFromCamera ( coords, camera )
参数 coords 表示鼠标的二维坐标,在归一化的设备坐标(NDC)中,也就是 X 和 Y 分量,应该介于 -1 和 1 之间。camera 表示射线起点处的相机,即把射线起点设置在该相机位置处。
点击事件大多通过鼠标触发,我们用鼠标点击显示区域的位置和当前场景使用的相机对象调用此对象,Three.js 会为我们计算出当前射线的位置。
.intersectObject ()
和 .intersectObjects ()
两个方法用来检查射线和物体之间的所有交叉点数据。
如果检测射线和一个对象是否相交,推荐使用 intersectObject()
,如果判断的是这个对象的子对象,那推荐使用 intersectObjects()
,将 3D 对象的 children 属性传入。
返回值是一个交叉点对象数组,且按距离排序,最接近的排在首位。
1
.intersectObject ( object, recursive, optionalTarget)
参数 object,用来检测和射线相交的物体。如果 recursive 设置为 true,还会向下继续检查所有后代,否则只检查该对象本身,缺省值为 false。optionalTarget 为可选参数,用于设置放置结果的数组,如果缺省,则将会实例化一个新数组,并将获取到的数据放入其中。
1
.intersectObjects ( array, recursive, optionalTarget)
intersectObject()
和 intersectObjects()
的区别在于第一个参数。intersectObject 的第一个参数为 3D 对象,而 intersectObjects 需要传入一个由 3D 对象组成的数组。
我们知道两个方法的返回值均为对象数组。接下来,我们再进一步了解下这个返回值。
如果射线与场景内的模型没有相交,将返回一个空数组,否则,将返回一个按从近到远顺序排列的对象数组,数组中每个对象的内容为:
1
[ { distance, point, face, faceIndex, indices, object }, ... ]
其中:
- distance:射线的起点到相交点的距离;
- point:在世界坐标中的交叉点;
- face:相交的面;
- faceIndex:相交的面的索引;
- indices:组成相交面的顶点索引;
- object:相交的对象。
当一个网孔(Mesh)对象和一个缓存几何模型(BufferGeometry)相交时,faceIndex 将是 undefined,并且 indices 将被设置;而当一个网孔(Mesh)对象和一个几何模型(Geometry)相交时,indices 将是 undefined。
当计算这个对象是否和射线相交时,Raycaster 把传递的对象委托给检测 3D 对象的 raycast 方法,该方法通过计算,检测出当前模型与射线是否相交。
这允许 Mesh 对光线投射的反应可以不同于 lines 和 pointclouds。Mesh、lines、pointclouds 是三种不同的计算相交的方法。Mesh 对射线与网格对象的每一个面进行相交判断。lines,则是判断两条线之间的距离,至于 pointclouds,则是通过 distanceSqToPoint 判断当前是否相交。
注意,对于网格,面(faces)必须朝向射线原点,这样才能被检测到。背面射线的交叉点将无法被检测到。
为了使光线能投射到一个对象的正反两面,你需要设置 material 的 side 属性为 THREE.DoubleSide。
模型点击事件的实现
上面讲解了射线的相关内容,接下来,我们来看一下,如何使用射线实现一个普通的点击事件。
首先,我们通过点击事件回调的 event 获取点击的位置:
1
2
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
默认没有经过矩阵转换过的显示区域的宽和高分别是 2,即中心点也是 WebGL 场景的坐标原点,左上角的坐标是 (-1.0, 1.0, 0.0)
,右下角的坐标是 (1.0, -1.0, 0.0)
。我们通过单击点的位置计算出当前该点在场景中,没有被矩阵转换过的平面坐标。如果 WebGL 的渲染区域没有占满窗口,我们还需获取显示区域距离窗口左上角的偏移量,再计算位置,计算方法如下:
1
2
3
4
5
6
7
8
9
10
11
//通过 dom 的 getBoundingClientRect 方法获得当前显示区域距离左上角的偏移量
var left = renderer.domElement.getBoundingClientRect().left;
var top = renderer.domElement.getBoundingClientRect().top;
//根据浏览器的设备类型来获取到当前点击的位置
var clientX = dop.browserRedirect() === "pc" ? event.clientX - left : event.touches[0].clientX - left;
var clientY = dop.browserRedirect() === "pc" ? event.clientY - top : event.touches[0].clientY - top;
//计算出场景内的原始坐标
mouse.x = (clientX / renderer.domElement.offsetWidth) * 2 - 1;
mouse.y = -(clientY / renderer.domElement.offsetHeight) * 2 + 1;
获取到坐标以后,我们需要使用射线的 setFromCamera()
方法配合场景坐标和相机更新射线的位置:
1
raycaster.setFromCamera( mouse, camera );
接着,使用 intersectObjects()
方法获取射线和所有模型相交的数组集合:
1
var intersects = raycaster.intersectObjects( scene.children );
这里再提醒一句,很多读者可能发现,有时点击后射线并未获取到相交的物体。这是因为我们一般使用 intersectObject()
和 intersectObjects()
时,只会传入对象,而有的模型由多个模型组成,也就是它的子类,这时我们需要传入第二个值,设置为 true,来提示 Three.js 遍历它的子类。
最后,如果有与射线相交的模型,返回的 intersects 数组的长度将不为零:
1
2
3
if(intersects.length > 0){
alert("有相交的模型");
}
这里提供一个点击案例,点中物体后,模型颜色将会变色:点击这里 。
案例源码地址:点击这里。
简单框选案例的实现
最近有些小伙伴想实现一个简单的框选案例,在这一篇中我来带大家完成。
本案例通过判断模型的位置实现框选,即框选前先获取所有模型在二维平面上的位置,然后再判断这些二维平面上的点是否处于框内。
相对于其它实现方式,这种实现节约性能,简单易懂,能够应付大部分场景。
接下来,我讲解一下这个框选的实现思路。
首先,编写以下代码,当鼠标按下时,记录鼠标按下时的场景坐标:
1
2
3
4
5
6
//获取到显示区域距离窗口左上角的偏移量
domClient.x = renderer.domElement.getBoundingClientRect().left;
domClient.y = renderer.domElement.getBoundingClientRect().top;
//计算出当前鼠标距离显示区域左上角的距离
down.x = e.clientX - domClient.x;
down.y = e.clientY - domClient.y;
前两行代码求出了当前显示区域距离窗口左上角的偏移量,后两行则计算出来了当前鼠标点击位置距离显示区域左上角的偏移。
接着,使用 box 对象方法计算出模型的包围盒中心位置,适用于多个复杂模型场景。如果只是简单几何体,可以直接使用 Mesh 的位置来计算。通过相机将世界坐标的位置转换为平面坐标,并将模型放到一个数组内以便后期使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (let i = 0; i < group.children.length; i++) {
let box = new THREE.Box3();
box.expandByObject(group.children[i]);
//获取到平面的坐标
let vec3 = new THREE.Vector3();
box.getCenter(vec3);
let vec = vec3.project(camera);
modelsList.push(
{
component: group.children[i],
position: {
x: vec.x * half.width + half.width,
y: -vec.y * half.height + half.height
},
normalMaterial: group.children[i].material
}
)
}
上面代码首先通过一个循环,计算出了每一模型在二维平面中的位置。
接下来,绑定 Document 的 mousemove 事件和 mouseup 事件。鼠标移动事件用来判断每个模型是否处于框内,鼠标抬起事件则将绑定的事件清除。
1
2
3
//绑定鼠标按下移动事件和抬起事件
document.addEventListener("mousemove", movefun, false);
document.addEventListener("mouseup", upfun, false);
在鼠标移动事件中,我们计算出当前四个边的位置,并且循环判断哪些模型的位置处于框内,处于框内的模型的材质将被修改为框选材质:
1
2
3
4
5
6
7
8
9
10
for (let i = 0; i < modelsList.length; i++) {
let position = modelsList[i].position;
//判断当前位置是否处于框内
if (position.x > min.x && position.x < max.x && position.y > min.y && position.y < max.y) {
modelsList[i].component.material = material;
}
else{
modelsList[i].component.material = modelsList[i].normalMaterial;
}
}
在最后的鼠标抬起事件内,将框选框隐藏,并将所有材质修改为默认材质:
1
2
3
4
5
6
7
8
9
10
11
12
function upfun(e) {
//清除事件
document.body.removeChild(div);
document.removeEventListener("mousemove", movefun, false);
document.removeEventListener("mouseup", upfun, false);
//将所有的模型修改为当前默认的材质
for (let i = 0; i < modelsList.length; i++) {
modelsList[i].component.material = modelsList[i].normalMaterial;
}
}
最后,附上可以查看的案例地址:点击这里。
案例源码地址:点击这里
Three.js 性能优化
在接触 Three.js 一段时间后,或多或少都会遇到性能问题。此类问题将直接导致页面帧率过低,严重时会导致页面崩溃。
导致性能问题的原因有很多,比如模型文件太大或顶点数太多导致加载时间过长,甚至失败,又或者代码书写逻辑有问题导致场景帧数太低。面对不同原因,我们需采取不同的应对方案,比如面对模型问题可对模型进行减面、减体积等处理;针对代码问题,则需尽量减少代码的运算。在平时的开发中,我们还要尽可能避免导致这些性能问题的根由。
下面,我将分享几种简单的有助于提升性能的方法。
尽量共用几何体和材质
当大批量进行模型渲染时,不可避免地会有大量重复几何体的创建,这时共用相同的几何体和材质可以减少内存和 GPU 的数据传输。
我们具体来看一个实例,比如你需要创建三百个简单的相同颜色的立方体模型,普通的实现方法如下:
1
2
3
4
5
6
7
8
for (let i = 0; i < 300; i++) {
let geometry = new THREE.BoxGeometry(10, 10, 10);
let material = new THREE.MeshLambertMaterial({color: 0x00ffff});
let mesh = new THREE.Mesh(geometry, material);
//随机位置
mesh.position.set(THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200));
group.add(mesh);
}
上面代码创建了三百个相同的几何体和材质,该方法中有很多不必要的创建过程,增加运算的同时,还浪费了内存。
要解决这些性能问题,创建时我们可以共用相同的几何体和材质,改良后的代码如下所示:
1
2
3
4
5
6
7
8
let geometry = new THREE.BoxGeometry(10, 10, 10);
let material = new THREE.MeshLambertMaterial({color: 0x00ffff});
for (let i = 0; i < 300; i++) {
let mesh = new THREE.Mesh(geometry, material);
//随机位置
mesh.position.set(THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200));
group.add(mesh);
}
利用上面代码,我们只需创建一套相同的几何体的顶点数据,不仅降低了内存消耗,还提高了添加运算效率。
模型删除时,材质和几何体也需从内存中清除
我们使用 remove()
将模型从场景内删除后,发现内存占用并没有太大变化。这是因为几何体和材质还保存在内存当中,这时需要手动调用 dispose()
方法将其从内存中删除。
下面为删除整个场景组的案例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//删除group
function deleteGroup(name) {
let group = scene.getObjectByName(name);
if (!group) return;
//删除掉所有的模型组内的mesh
group.traverse(function (item) {
if (item instanceof THREE.Mesh) {
item.geometry.dispose(); //删除几何体
item.material.dispose(); //删除材质
}
});
scene.remove(group);
}
使用 merge 方法合并不需要单独操作的模型
在 Three.js 新版本中,将 merge 方法整合在了几何体上,主要应用于拥有大量几何体且材质相同的模型上。我们可以将多个几何体拼接成单个整体的几何体,从而达到节约性能的目的,但该做法的缺点则是失去了对单个模型的控制。
通过下面代码,我们了解下 merge 的使用方法:
1
2
3
4
5
6
7
8
9
10
11
//合并模型,则使用merge方法合并
var geometry = new THREE.Geometry();
//merge方法将两个几何体对象或者Object3D里面的几何体对象合并,(使用对象的变换)将几何体的顶点、面、UV 分别合并。
//THREE.GeometryUtils: .merge() has been moved to Geometry. Use geometry.merge( geometry2, matrix, materialIndexOffset ) instead. 如果新版本用老版本的会报这个错
for(var i=0; i<20000; i++){
var cube = addCube(); //创建了一个随机位置的几何体模型
cube.updateMatrix(); //手动更新模型的矩阵
geometry.merge(cube.geometry, cube.matrix); //将几何体合并
}
scene.add(new THREE.Mesh(geometry, cubeMaterial));
我们再看一个案例:点击这里。
在上面案例中,在不选中 combined 的前提下 ,选择 redraw 20000 个模型的话,一般只有十几帧的帧率。但如果选中了 combined,会发现渲染的帧率可达到满帧(60帧),性能得到了巨大提升。
在循环渲染中避免使用更新
几何体、材质、纹理等相关数据的更新尽量不要放到循环渲染中进行。循环渲染时,每一帧都会对 GPU 数据进行更新,这将引发很多不必要的数据更新过程。
接下来,我们了解下几何体、材质、纹理数据更新的相关属性。
- 几何体
1
2
3
4
5
6
geometry.verticesNeedUpdate = true; //顶点发生了修改
geometry.elementsNeedUpdate = true; //面发生了修改
geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
geometry.uvsNeedUpdate = true; //uv映射发生了修改
geometry.normalsNeedUpdate = true; //法向发生了修改
geometry.colorsNeedUpdate = true; //顶点颜色发生的修改
- 材质
1
material.needsUpdate = true;
- 纹理
1
texture.needsUpdate = true;
如果它们发生更新,则将其设置为 true,Three.js 会通过判断,将数据重新传输到显存当中,并将配置项重新修改为 false。这是一个十分影响运行效率的过程,我们尽量只在需要的时候修改,且不要放到 render()
方法当中循环设置。
只在需要的时候渲染
在没有操作的时候,一直循环渲染属于浪费资源。接下来我带给大家一个只在需要时渲染的方法。
首先在循环渲染中加入一个判断,如果判断值为 true 时,才可以循环渲染:
1
2
3
4
5
6
7
8
9
10
11
var renderEnabled;
function animate() {
if (renderEnabled) {
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
}
animate();
然后设置一个延迟器函数,每次调用后,可以将 renderEnabled 设置为 true,并延迟三秒将其设置为 false,这个延迟时间大家可以根据需求来修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//调用一次可以渲染三秒
let timeOut = null;
function timeRender() {
//设置为可渲染状态
renderEnabled = true;
//清除上次的延迟器
if (timeOut) {
clearTimeout(timeOut);
}
timeOut = setTimeout(function () {
renderEnabled = false;
}, 3000);
}
接下来,我们在需要的时候调用这个 timeRender()
方法即可,比如在相机控制器更新后的回调中:
1
2
3
controls.addEventListener('change', function(){
timeRender();
});
如果相机位置发生变化,就会触发回调,开启循环渲染,更新页面显示。
如果我们添加了一个模型到场景中,直接调用重新渲染即可:
1
2
scene.add(mesh);
timeRender();
最后一个重点问题,就是材质的纹理为异步渲染,需要在图片添加完成后,触发回调。好在 Three.js 已经考虑到了这一点,Three.js 的静态对象 THREE.DefaultLoadingManager
的 onLoad 回调会在每一个纹理图片加载完成后触发回调。依靠它,我们可以在 Three.js 的每一个内容发生变更后触发重新渲染,且闲置状态下停止渲染。
1
2
3
4
//每次材质和纹理更新,触发重新渲染
THREE.DefaultLoadingManager.onLoad = function () {
timeRender();
};
Three.js 核心对象
本文是介绍 Three.js 核心技术知识的最后一节,带大家了解经常接触到的对象。第16课,也是课程的最后一节,我们就开始实战演练。
THREE.Clock
时间对象用于跟踪时间,用于计算每一帧的渲染时间或者从开始渲染到结束的时间。
构造函数
构造函数的使用,如下所示:
1
var clock = new THREE.Clock();
实例化时间对象,还可以接收一个布尔类型的参数,用于设置实例化完成后,是否启动时间对象计时,默认为开启。
方法
该类主要有以下几种方法。
- .start():此方法用于启动时间对象开始计时,将所有的信息重置;
- .stop():停止时间对象的计时;
- .getElapsedTime():此方法返回自时间对象开启以后到现在的时间;
- .getDelta():此方法返回上次调用到这次调用此方法的间隔时间,用于计算渲染间隔。
THREE.Color
THREE.Color 为颜色类,用到颜色的地方,都可以实例化此类,如设置材质颜色、场景背景颜色等。
构造函数
我们可以通过 new THREE.Color()
传入一个值来初始化一个颜色对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//不传参数将默认渲染成白色
var color = new THREE.Color();
//十六进制颜色(推荐)
var color = new THREE.Color( 0xff0000 );
//RGB 字符串
var color = new THREE.Color("rgb(255, 0, 0)");
var color = new THREE.Color("rgb(100%, 0%, 0%)");
//支持所有140个颜色名称
var color = new THREE.Color( 'skyblue' );
//HSL 字符串
var color = new THREE.Color("hsl(0, 100%, 50%)");
//支持区间为从0到1的RGB数值
var color = new THREE.Color( 1, 0, 0 );
方法
该类主要有以下几种方法。
- .clone():返回一个相同颜色的颜色对象;
- .add():将一个颜色对象的颜色值和此颜色对象的颜色相加;
- .multiply():将一个颜色对象的颜色值和此颜色对象的颜色相乘;
- .set():重新设置颜色对象的颜色,支持实例化时的所有方式。
矢量对象
矢量是 GLSL ES 语言内的标准数据类型,也是 WebGL 原生着色器语言的数据类型。而 Three.js 为了方便和原生融合,内置了 THREE.Vector2
、THREE.Vector3
和 THREE.Vector4
三种矢量,分别代表二维向量、三维向量和四维向量。
二维矢量
二维矢量一般表示二维平面上点的位置和方向。
下面代码生成了一个普通的二维矢量:
1
var a = new THREE.Vector2( 0, 1 );
我们也可以通过 set()
方法,重新修改二维矢量的值:
1
a.set(2, 3);
三维矢量
在项目当中,我们经常会用到三维矢量,比如模型的位置属性、缩放属性,都是一个三维矢量。它由三个有序的数字组成的。
使用三维矢量,我们可以表示:
- 三维空间中一个点的位置;
- 三维空间中两点连线的方向和长度;
- 任意有序的三个数字。
我们看下面这个例子:
1
2
3
4
5
6
7
8
//创建一个普通的三维矢量
var a = new THREE.Vector3( 0, 1, 0 );
//默认空值,将会重置为(0, 0, 0)
var b = new THREE.Vector3( );
//d矢量是从a点到b点的方向和长度
var d = a.distanceTo( b );
我们经常进行的模型位置改变以及缩放操作,其实都是通过修改三维矢量的值来实现的,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
//创建一个三维矢量
var vec = new THREE.Vector3(0, 1, 0);
//修改模型的位置
mesh.position = vec;
//修改三维矢量的值
vec.set(1, 2, 1);
//修改模型的缩放值
mesh.scale.set(2, 2, 2);
我们还可以修改三维向量的单个值:
1
2
3
4
5
vec.setComponent(0, 20); //将三维向量的第一值修改为20
.setComponent(index, value);
//index 修改的单个值的下标,可选值为 0 1 2 对应 vec.x vec.y vec.z
//value 修改成的数字
四维矢量
四维矢量是由四个数字组成的数据类型,四个值分别代表 x、y、z、w。
四维矢量可以表示的内容有:
- 思维空间内的一个点;
- 思维空间里的方向和长度;
- 任意有序的四个数字。
我们再看下面这个例子:
1
2
3
4
5
//创建一个普通的四维矢量
var a = new THREE.Vector4( 0, 1, 0, 0 );
//如果不设置参数,默认值为 (0, 0, 0, 1)
var b = new THREE.Vector4( );
四维矩阵
矩阵是高等代数中的常见工具,也常见于统计分析等应用数学学科中。Three.js 为我们封装了三维矩阵(3x3)四维矩阵(4x4)。
篇幅有限,这里就不介绍三维矩阵了,我们主要看下四维矩阵。
在 3D 计算机图形中,最常见的四维矩阵主要用于转换矩阵。这代表三维空间中的点 Vector3 通过乘以转换矩阵,可以实现如平移、旋转、剪切、缩放、发射、正交或透视投影等变化。
在每个 3D 对象中都有三个四维矩阵。
- Object3D.matrix:存储 3D 对象的局部变换,这是 3D 对象相对于父对象的转换;
- Object3D.matrixWorld:3D 对象的全局或世界变换,如果 3D 对象没有父对象,则当前矩阵和其局部变换矩阵相同。
- Object3D.modelViewMatrix:表示 3D 对象相对于摄像机的坐标转换,对象的 modelViewMatrix 是对象的 matrixWorld 再乘以相机的 matrixWorldInverse 获得的结果。
相机对象不但含有 3D 对象的四维矩阵,还额外拥有以下两个四维矩阵。
- Camera.matrixWorldInverse:视图矩阵(相机的 matrixWorld 反转后的四维矩阵);
- Camera.projectionMatrix:表示场景内的模型如何转换到二维显示区域的变换矩阵。
如果你对矩阵的实现原理感兴趣的话,可点击这里查看,这里不再过多解释原理。
接下来我们查看一下 Three.js 封装的四维变换矩阵为我们提供了哪些方法。
1
2
3
4
5
6
7
8
9
10
11
12
var m = new THREE.Matrix4();
//我们可以通过手动设置每一项的数值来生成变换矩阵
m.set( 11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44 );
//创建一个矢量,并应用此变换矩阵
var v = new THREE.Vector3();
v.applyMatrix4(m); //应用变换矩阵
上面代码创建了一个默认的四维矩阵,三维矢量乘以默认的四维矩阵将不会产生变化。
我们可以使用 .identity()
方法来重置变换矩阵:
1
2
var m = new THREE.Matrix4();
m.identity(); //重置矩阵恢复默认
我们可以通过 .lookAt()
传入一个三个矢量生成一个旋转矩阵。这三个矢量分别代表眼的位置、查看的物体位置和向上的方向:
1
2
var m = new THREE.Matrix4();
m.lookAt(eye, center, up); //生成旋转变换矩阵
我们也可以通过旋转弧度来生成旋转变换矩阵:
1
2
3
4
5
6
7
8
9
10
11
12
var m = new THREE.Matrix4();
//通过绕x轴旋转生成变换矩阵
m.makeRotationX(Math.PI/2); //绕x轴旋转90度变换矩阵
m.makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI/2); //效果同上
//通过绕y轴旋转生成变换矩阵
m.makeRotationY(Math.PI); //绕y轴旋转180度变换矩阵
m.makeRotationAxis(new THREE.Vector3(0, 1, 0), Math.PI); //效果同上
//通过绕z轴旋转生成变换矩阵
m.makeRotationZ(Math.PI/4); //绕z轴旋转45度变换矩阵
m.makeRotationAxis(new THREE.Vector3(0, 0, 1), Math.PI/4); //效果同上
通过设置缩放来生成缩放变换矩阵:
1
2
3
var m = new THREE.Matrix4();
m.makeScale(1, 2, 5); //生成沿x轴不变,y轴放大两倍,z轴放大五倍的缩放变换矩阵
通过设置位置平移来生成变换矩阵:
1
2
3
var m = new THREE.Matrix4();
m.makeTranslation(10, -20, 100); //生成沿x正轴偏移10,y负轴偏移20,z正轴偏移100的平移变换矩阵
欧拉角
欧拉角通过在每个轴指定旋转的弧度和指定旋转轴的先后顺序来进行旋转变换。之前介绍场景时已做介绍,不再赘述。这里想说明的是,每个 3D 对象的 rotation 属性都是一个欧拉角对象。
创建欧拉角定义方法如下:
1
var e = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
前三个值分别代表每个轴上旋转的弧度,第四个值是应用旋转的顺序。默认为“XYZ”,这意味着对象将首先围绕其 X 轴旋转,然后围绕其 Y 轴旋转,最后围绕其 Z 轴旋转。其他可能性有 YZX、ZXY、XZY、YXZ、ZYX,都必须大写。
修改欧拉角的方法如下:
1
2
3
//通过set方法重新设置
var e = new THREE.Euler();
e.set(Math.PI, 0, - Math.PI / 2, "YZX"); //先沿y轴旋转180度,再沿z轴旋转0度,最后沿x轴逆时针旋转90度,第四个值可以不填,默认是"XYZ"
我们也可以通过变换矩阵来修改当前的欧拉角:
1
2
3
4
var e = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
var m = new THREE.Matrix4();
e.setFromRotationMatrix(m); //在当前的旋转角度,再进行矩阵变换
结语
Three.js 知识讲解部分,到这里基本就结束了。第一次写正式的教程,难免有一些瑕疵。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+Threejs学习和总结进阶篇 - AIGISSS Threejs学习和总结进阶篇
本文最后更新于:1 年前
相机控制器Controls
相机的基本内容我们已经了解。
正常的项目中,大家的需求都是不一样的,又通常会碰上需求中途改变的情况,我们之前做的简易版相机控制器很难满足此类项目对相机的操作需求。而且,造轮子的前提是当前的框架以及插件已经无法满足自身的需求时,才会考虑造轮子。要不然,项目的进度会被拖得很慢,甚至有可能因此而错过红利期。
好在 Three.js 官方和同道中的朋友们给我们提供了很多相关的插件,我们可以根据需求引入相关的插件来实现需求,本文我们就来看一下官方案例中提供的相机控制器。
从官网下载的代码包里可以发现有很多的相机控制器,文件夹地址为:/examples/js/controls/
,里面的文件插件都是和控制相机和控制模型相关的插件,我们罗列一下相关插件:
- DeviceOrientationControls:陀螺仪相机控制器,实现移动端陀螺仪控制相机。
- DragControls:控制鼠标拖拽移动物体的功能。
- EditorControls:实现相机的旋转、缩放、平移功能,相对于 OrbitControls 的功能差不少,不建议使用。
- FirstPersonControls:第一视角相机控制器。
- FlyControls:飞行相机控制器。
- OrbitControls:轨道控制器。
- OrthographicTrackballControls:正交轨迹球控制器——正交相机使用的轨迹球控制器。
- TrackballControls:轨迹球控制器——透视相机使用的轨迹球控制器。
- PointerLockControls:鼠标锁定相机控制器。
- TransformControls:控制模型位置、缩放、旋转的控制器。
- VRControls:实现 VR 双屏相机控制器。
由于篇幅有限,上面的控制器无法一一介绍,我们将重点介绍三种常用的相机控制器。
OrbitControls
OrbitControls 控制器是我们常用的相机控制器,它的功能丰富,使用简单,为大多数项目的使用插件。
使用操作
使用 OrbitControls 控制器我们可以实现旋转、缩放、平移等功能,下面简单列一下 OrbitControls 控制器的操作方法:
- 围绕焦点旋转:使用鼠标左键拖拽;
- 放大和缩小:使用鼠标中键按住拖拽或者鼠标中键滑动滚轮;
- 平移相机:按住鼠标右键拖拽或者使用键盘的上下左右键。
控制器引入
在项目中使用 OrbitControls 控制器,可分为下面几步。
- 首先,将插件文件引入到项目中:
1
<script src="../js/OrbitControls.js"></script>
- 然后,通过相机和渲染器的 Dom 对象实例化相机:
1
control = new THREE.OrbitControls(camera, renderer.domElement);
- 最后,在每一帧渲染里面更新相机的位置:
1
2
3
4
function render() {
control.update();
renderer.render(scene, camera);
}
这样,我们就完成了一个最简单的 OrbitControls 控制器使用。
属性和方法
OrbitControls 控制器最大的优势就是有丰富的配置项,供我们修改来实现项目中的需求,接下来我们看看有哪些属性配置。
属性 描述 enabled 是否开启当前控制器,默认值是 True,如果设置为 False,将无法通过操作修改相机。 target 控制器的焦点位置,是一个 THREE.Vector3
对象,默认是 (0, 0, 0)
minDistance 相机距离焦点的最近距离,默认值是0。 此属性适用于透视相机 PerspectiveCamera。 maxDistance 相机距离焦点的最远距离,默认值是 Infinity(无限远), 此属性适用于透视相机 PerspectiveCamera。 minZoom 相机距离焦点的最近距离,默认值是0,此属性适用于正交相机 OrthographicCamera。 maxZoom 相机距离焦点的最远距离,默认值是 Infinity(无限远),此属性适用于正交相机 OrthographicCamera。 minPolarAngle 相机位置和焦点与焦点和最上方组成的最小夹角限制,默认值是0。 maxPolarAngle 相机位置和焦点与焦点和最上方组成的最大夹角限制,默认值是 Math.PI,也就是180度角。 minAzimuthAngle 当前相机沿水平方向顺时针旋转的弧度,默认值是 - Infinity
。 maxAzimuthAngle 当前相机沿水平方向逆时针旋转的弧度,默认值是 Infinity
。 enableDamping 是否开启拖拽惯性移动,即拖拽停止相机会有缓慢停止的距离移动,默认值是 False。 dampingFactor 拖拽惯性移动的阻力,默认值是 0.25。 enableZoom 是否开启缩放操作,默认值是 True。 zoomSpeed 缩放速度,默认值是 1.0。 enableRotate 是否开启相机绕焦点旋转操作,默认值是 True。 rotateSpeed 旋转速度,默认值是 1.0。 enablePan 是否开启相机平移操作,默认值是 True。 panSpeed 平移的速度,默认值是 1.0。 screenSpacePanning 修改相机平移的方向,默认值是 False,即沿 x 轴正负方向和 y 轴正负方向移动。可选值是 True,可以修改为沿 x 轴正负方向和 y 轴正负方向移动。 keyPanSpeed 键盘上下左右键移动相机的速度,默认值是 7.0。 autoRotate 当前相机是否自动旋转,默认值是 False,不自动旋转。 autoRotateSpeed 自动旋转的速度,默认值是 2.0,即渲染满60帧的情况下30秒旋转360度。 enableKeys 是否开启键盘控制先机平移,默认值是 True。
OrbitControls 控制器的属性配置介绍完了,我们看看 OrbitControls 控制器还有那些方法。
update()
OrbitControls 控制器更新相机的方法,需要在每一帧里面调用。
reset()
重置方法,相机回到初始位置。
dispose()
销毁当前实例化的 OrbitControls 控制器。
change 回调
我们还可以监听相机改变回调,如果控制器修改了相机,将会产生一个回调:
1
2
3
controls.addEventListener('change', function(){
console.log("相机动了!");
});
最后,我附上 OrbitControls 控制器案例:
也可以从这里获取案例源码:
TrackballControls
TrackballControls 控制器比 OrbitControls 控制器更自由,TrackballControls 控制器能够沿焦点进行球形旋转,没有死角,但比 OrbitControls 控制器少一些相关的功能配置。如何选择使用它们还是看项目需求,接下来还是先看如何操作。
注意,透视相机和正交相机使用的不是一个插件,此插件为透视相机使用,如果是正交相机请使用 OrthographicTrackballControls。
使用操作
使用 TrackballControls 控制器我们可以实现旋转、缩放、平移等功能,下面说一下如何使用 TrackballControls 控制器进行操作:
- 围绕焦点旋转:使用鼠标左键拖拽;
- 放大和缩小:使用鼠标中键按住拖拽或者鼠标中键滑动滚轮;
- 平移相机:按住鼠标右键拖拽或者使用键盘的上下左右键。
TrackballControls 控制器和 OrbitControls 控制器的操作相同,没什么可说的。
控制器引入
在项目中使用 TrackballControls 控制器和 OrbitControls 控制器的方法雷同,分为下面几步。
- 首先,将插件文件引入到项目中:
1
<script src="../js/TrackballControls.js"></script>
- 然后,通过相机和渲染器的 Dom 对象实例化相机:
1
control = new THREE.TrackballControls(camera, renderer.domElement);
- 最后,在每一帧渲染里面更新相机的位置:
1
2
3
4
function render() {
control.update();
renderer.render(scene, camera);
}
属性和方法
属性 描述 enabled 是否开启当前控制器,默认值是 True,如果设置为 False,将无法通过操作修改相机。 rotateSpeed 控制相机旋转速度,默认值是 3.0。 zoomSpeed 控制相机缩放速度,默认值是 1.2。 panSpeed 控制相机平移速度,默认值是 0.3。 noRotate 关闭相机旋转,默认 False,开启。 noZoom 关闭相机缩放,默认 False,开启。 noPan 关闭相机移动,默认 False 开启。 staticMoving 关闭拖拽惯性移动 默认值 False,开启。 dynamicDampingFactor 拖拽惯性移动阻力,默认值是 0.2。 minDistance 相机距离焦点的最近距离,默认值是 0。 maxDistance 相机距离焦点的最远距离,默认值是 Infinity(无限远)。
相对于 OrbitControls 控制器,TrackballControls 控制器的属性少一些,但是相关的功能还是比较全面的。TrackballControls 控制器的方法也和 OrbitControls 控制器的方法雷同。
update()
TrackballControls 控制器更新相机的方法,需要在每一帧里面调用。
reset()
重置方法,相机回到初始位置。
dispose()
销毁当前实例化的 TrackballControls 控制器。
change 回调
我们还可以监听相机改变回调,如果控制器修改了相机,将会产生一个回调:
1
2
3
controls.addEventListener('change', function(){
console.log("相机动了!");
});
最后,附上 TrackballControls 控制器案例:
也可以从这里获取案例源码:
DeviceOrientationControls
最后,我们介绍的这个控制器只兼容含有陀螺仪的移动端。DeviceOrientationControls 控制器可以通过获取设备的陀螺仪状态来控制相机的朝向。
如果你还对陀螺仪不了解,请点击查看这里,在这里不多说。
DeviceOrientationControls 的内容配置较少,我们先看一下案例。
使用手机打开网址:点击这里 ,然后手机朝下然后移动,你会发现能够通过手机的转向来控制相机的朝向,是不是很神奇。接下来我们看看如何引入到项目中。
- 首先,将插件文件引入到项目中:
1
<script src="../js/DeviceOrientationControls.js"></script>
- 然后,通过相机对象实例化相机:
1
control = new THREE.DeviceOrientationControls(camera);
- 最后,在每一帧渲染里面更新相机的位置:
1
2
3
4
function render() {
control.update();
renderer.render(scene, camera);
}
这样我们就完成了对 DeviceOrientationControls 控制器的添加。
DeviceOrientationControls 控制器相关的配置也很少,只有一个 Enabled 属性,设置为 True,则控制器会更新相机的位置,反之,设置 False 将无法更新相机位置。
还有一个方法就是销毁当前控制器的方法:
1
controls.dispose(); //销毁当前控制器
最后,附上源码:
模型加载Loaders
现在市面上的 3D 模型有上百种,每一种格式都有不同的用途,不同的功能和复杂程度。尽管 Three.js 提供了很多的加载器,但选择正确的格式和工作流程将为以后的工作节省大量时间和成本。而且某些格式难以使用,效率低下,甚至有些目前还未完全被支持。
推荐使用的模型格式
官方推荐我们使用的 3D 模型的格式为 glTF,由于 glTF 专注于传输,因此它的传输和解析的速度都很快。glTF 模型的功能包括网格、材质、纹理、蒙皮、骨骼、变形动画、骨骼动画、灯光以及相机。
如果当前的首选不是 glTF 格式,那么推荐使用 Three.js 定期维护并且流行的格式 FBX、OBJ 或者 COLLADA 格式,Three.js 也有自己独有的 JSON 格式。我们接下来将介绍这五种格式。
Three.js 的 JSON 格式
这里的 JSON 格式指的是 Three.js 可以将其转换为场景 3D 对象的 JSON 格式模型。这种格式内部一般必有的四项为:
- metadata:当前模型的相关信息以及生成的工具信息;
- geometries:存储当前模型所使用的几何体的数组;
- materials:存储当前模型所使用的材质的数组;
- object:当前模型的结构以及标示所应用到的材质和几何体标示。
所有的模型网格,几何体和材质都有一个固定的 UUID 标识符,在 JSON 格式中均通过 UUID 引用。
3D 对象转成 JSON
所有的 THREE.Object3D
对象都可以转成 JSON 字符串保存成为文件,但我们不能直接将对象转成 JSON,因为 JSON 无法保存函数。Three.js 给我们提供了一个 toJSON() 的方法来让我们将其转换为可存储的 JSON 格式。
1
2
3
4
var obj = scene.toJSON(); //将整个场景的内容转换成为 JSON 对象
var obj = group.toJSON(); //将一个模型组转成 JSON 对象
var obj = mesh.toJSON(); //将一个模型网格转成 JSON 对象
var JSONStr = JSON.stringify(obj); //将 JSON 对象转换成 JSON 字符串
按照这种方式,我们就可以将生成的场景模型保存为文件。
使用 ObjectLoader 加载 JSON 模型
这里我们将使用到 Three.js 内置的对象 THREE.ObjectLoader
加载模型。
直接加载 Three.js 生成的 JSON 对象,代码如下:
1
2
3
4
var obj = scene.toJSON(); //将整个场景的内容转换成为json对象
let loader = new THREE.ObjectLoader(); //实例化ObjectLoader对象
let scene = loader.parse(obj); //将json对象再转换成3D对象
加载外部的 JSON 文件:
1
2
3
4
5
6
let loader = new THREE.ObjectLoader(); //实例化ObjectLoader对象
//加载模型,并在回调中将生成的模型对象添加到场景中
loader.load("../js/models/json/file.json", function (group) {
scene.add(group);
});
案例地址:请点击这里。
案例的右上角有四个点击事件:
- 添加模型:将在场景内随机生成一组立方体,每次都不相同。
- 导出模型:将场景内这一组立方体导出到本地 JSON 文件。
- 导入模型:可以选择将符合 JSON 文件作解析并导入到场景内。
- 加载模型:将加载服务器上面的一个 JSON 文件。
案例代码地址:请点击这里。
glTF 格式文件导入
glTF 格式的 3D 格式文件是官方推荐的使用格式,这种格式的文件我们可以在 Sketchfab 官网下载,这是一个国外比较知名的模型网站。
- 下载地址:点击这里。
我们可以在这里下载一些免费的 glTF 格式的模型。
我在官网上找了个不错的模型做了一个案例,点击这里查看。

模型加载的速度会有些慢,大家可以等待下便能够看到这个小汽车。
接下来我们便讲解一下加载 glTF 模型的流程。
- 首先,将 GLTFLoader 加载器插件引入到页面,插件在官方包的
/examples/js/loaders/
文件夹中,一些文件的导入插件都在这个文件夹内,大家有兴趣可以研究一下:
1
<script src="../js/loaders/GLTFLoader.js"></script>
- 然后创建一个加载器:
1
var loader = new THREE.GLTFLoader();
- 使用加载器加载模型,并调节一下模型大小在场景内展示:
1
2
3
4
loader.load('../js/models/gltf/scene.gltf', function (gltf) {
gltf.scene.scale.set(.1,.1,.1);
scene.add(gltf.scene);
});
有时候我们可能不明白,我加载了一个模型,哪一部分是需要导入场景的模型呢?
这里我们可以先将解析的出来的模型对象打印一下,然后通过查看对象属性来了解导入场景内的对象,就比如 glTF 模型转换出来的对象的 scene 属性就是需要导入场景的对象,而 JSON 格式的模型是直接可以导入的对象。
模型加载案例源码:请点击这里。
FBX 模型导入
FBX 最大的用途是,在诸如 Max、Maya、Softimage 等软件间进行模型、材质、动作和摄影机信息的互导,这样就可以发挥 Max 和 Maya 等软件的优势。可以说,FBX 是最好的互导方案。

接下来我们看一个 FBX 模型导入的案例,是我在网上下载的一个 FBX 格式的模型,导入到场景内的效果:请点击这里查看。
接下来,我们看下它的实现过程。
首先我们需要导入 FBXLoader 插件,并且还需要额外增加一个解析二进制文件的插件 inflate.min.js
,不导入该文件的话,除了一些字符串存储的 FBX 格式,别的格式都会报错:
1
2
<script src="../js/loaders/inflate.min.js"></script>
<script src="../js/loaders/FBXLoader.js"></script>
创建 FBX 加载器:
1
var loader = new THREE.FBXLoader();
修改模型大小,并设置每个模型网格可以投射阴影:
1
2
3
4
5
6
7
8
9
10
loader.load('../js/models/fbx/file.fbx', function (fbx) {
fbx.scale.set(.1,.1,.1);
fbx.traverse(function (item) {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
scene.add(fbx);
});
这样就实现了 FBX 模型的导入。
案例源码地址:请点击这里。
OBJ 格式模型导入
OBJ 文件是 3D 模型文件格式。由 Alias|Wavefront 公司为 3D 建模和动画软件 Advanced Visualizer 开发的一种标准,适合用于 3D 软件模型之间的互导,也可以通过 Maya 读写。
OBJ 文件是一种文本文件,可以直接用写字板打开进行查看和编辑修改,但不包含动画、材质特性、贴图路径、动力学、粒子等信息。
OBJ 文件的导出通常会和 MTL 格式一同导出,MTL 作为 OBJ 文件的附属文件,却有着 OBJ 文件需要的贴图材质,所以,我们通常使用时,将它们两个文件一同导入。

这是我使用官网提供的一个模型制作的一个案例,查看地址:请点击这里。
我们看下实现导入的过程。
首先,我们需要将 OBJLoader 插件和 MTLLoader 插件引入页面:
1
2
<script src="../js/loaders/OBJLoader.js"></script>
<script src="../js/loaders/MTLLoader.js"></script>
实例化 MTLLoader :
1
2
3
4
//创建MTL加载器
var mtlLoader = new THREE.MTLLoader();
//设置文件路径
mtlLoader.setPath('../js/models/obj/');
如果有需要,我们还可以设置纹理文件夹地址:
1
2
//设置纹理文件路径
mtlLoader.setTexturePath('../js/models/obj/');
加载 MTL 文件,并在文件加载成功后,创建 OBJLoader 并设置对象应用当前的材质:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//加载mtl文件
mtlLoader.load('female02.mtl', function (material) {
//创建OBJ加载器
var objLoader = new THREE.OBJLoader();
//设置当前加载的纹理
objLoader.setMaterials(material);
objLoader.setPath('../js/models/obj/');
objLoader.load('female02.obj', function (object) {
//添加阴影
object.traverse(function (item) {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
//缩放
object.scale.set(.3,.3,.3);
scene.add(object);
})
});
我们再去加载 OBJ 文件,加载成功的文件就是可以导入到场景内的 3D 对象。
案例源码查看地址:请点击这里。
COLLADA 模型导入
COLLADA 是一个开放的标准,最初用于 3D 软件数据交换,由 SCEA 发起,现在则被许多著名厂家(如 Autodesk、XSI 等)支持。COLLADA 不仅仅可以用于建模工具之间的数据交换,也可以作为场景描述语言用于小规模的实时渲染。
COLLADA DOM 拥有丰富的内容用于表现场景中的各种元素,从多边形几何体到摄像机无所不包。我们可以通过 COLLADA DOM 库来进行场景文件的读取与处理操作。

上面是我写的一个模型导入案例,案例地址:请点击这里。
我们看下实现步骤。
首先引入 ColladaLoader 插件:
1
<script src="../js/loaders/ColladaLoader.js"></script>
接着实例化 ColladaLoader 对象:
1
var loader = new THREE.ColladaLoader();
最后加载文件并调整文件大小,添加到场景内:
1
2
3
4
5
6
7
8
9
10
11
12
13
loader.load('../js/models/collada/elf.dae', function (collada) {
//添加阴影
collada.scene.traverse(function (item) {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
//缩放
collada.scene.scale.set(5,5,5);
scene.add(collada.scene);
});
案例源码查看:请点击这里。
注意事项
1. 如何知道,加载完成的模型需要将哪部分导入到场景?
一般情况下都是将自身导入,比如 FBX,OBJ,JSON 等,还有一种,会在里面生成一个可导入 scene 属性,如 glTF 和 COLLADA 文件。如果导入哪部分你无法确定,你可以把模型对象打印到控制台查看,然后尝试往场景内导入。
2.导入到场景内的模型无法查看,而且也没有报错,为什么?
这种情况可能由多种情况造成的,一般主要有下面两种情况:
- 模型太小或者太大,这种情况可以尝试放大一千倍或者缩小一千倍来查看效果。
- 模型的位置太偏,根本不在相机照射范围内,这种问题我们可以将模型居中到相机照射的焦点位置查看,如何居中我们将在后面的课中讲解。
Three.js 动画
动画一般可以分为两种:一种是变形动画,另一种是骨骼动画。下面,我们先介绍一下变形动画。
变形动画
变形动画,通过修改当前模型的顶点位置来实现。比如,一个动画需要变动十次才可以实现,那么我们需要为当前模型的每一个顶点定义每一次所在的位置,Three.js 通过这一次次的修改实现了动画的整个流程。
为了帮助大家更好地理解变形动画的实现与使用,我创建了一个案例,查看地址为:点击这里。
在这个案例的右上角,我们能发现两个可切换的拖拽条。这两个拖拽条对应的是两个变形目标数组,拖拽范围是0-1,即当前的变形目标对本体的影响程度。拖拽它们,可发现界面中的立方体也会跟随之变动,从而影响当前的立方体。接下来我讲解一下,该案例的实现过程。
首先,创建模型的几何体,并为几何体 morphTargets 赋值两个变形目标。morphTargets 是一个数组,我们可以为其增加多个变形目标。在给 morphTargets 添加变形目标时,需要为其定义一个名称和相关的顶点,这个顶点数据必须和默认的模型的顶点数据保持一致,设置完后,我们需要调用 geometry 的 computeMorphNormals()
进行更新,代码如下:
1
2
3
4
5
6
7
8
9
10
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
// 创建两个影响立方体的变形目标
var cubeTarget1 = new THREE.BoxGeometry(2, 10, 2);
var cubeTarget2 = new THREE.BoxGeometry(8, 2, 8);
// 将两个geometry的顶点放入到立方体的morphTargets里面
cubeGeometry.morphTargets[0] = {name: 'target1', vertices: cubeTarget1.vertices};
cubeGeometry.morphTargets[1] = {name: 'target2', vertices: cubeTarget2.vertices};
cubeGeometry.computeMorphNormals();
然后,为当前模型设置材质,变形目标作为参数之一,可以使其变形。
1
var cubeMaterial = new THREE.MeshLambertMaterial({morphTargets: true, color: 0x00ffff});
接着,将创建好的网格模型添加到场景中。这时可以在 mesh 对象中找到 morphTargetInfluences 配置项,它也是一个数组,和 geometry 的 morphTargets 相对应,主要用来设置当前变形目标对本体的影响度,默认值为0-1,0为不影响本体,1为完全影响本体:
1
2
3
4
5
6
7
8
gui = {
influence1:0.01,
influence2:0.01,
update : function () {
cube.morphTargetInfluences[0] = gui.influence1;
cube.morphTargetInfluences[1] = gui.influence2;
}
};
至此,我们就手动实现了一个变形动画。在这个过程中,我们发现,变形动画是由于不断修改变形目标对本体的影响度而产生的。我们可以通过这个原理实现其他变形动画。
案例代码查看地址:请点击这里。
骨骼动画
实现骨骼动画,我们需要生成一个与模型相关的骨架。骨架中的骨骼与骨骼之间存在关联,模型的每一个要动的顶点需要设置影响它的骨骼以及骨骼对顶点的影响度。
和变形动画相比,骨骼动画更复杂一些,但又有更多的灵活性。使用变形动画,我们需要把所有的每一次的变动都存在一个顶点数组中,而骨骼动画,只需要设置骨骼的相关信息,就可以实现更多的动画。
下面我们看一个骨骼动画的简单案例:点击这里

这是官方提供的一个案例。我对其做了些简单修改,以显示出当前一个柱形图形的骨骼。实现起来比较复杂,我们需要先理解它是怎么实现的。
首先, 我们创建了一个圆柱几何体,通过圆柱的几何体每个顶点的 y 轴坐标位置来设置绑定的骨骼的下标和影响的程度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//遍历几何体所有的顶点
for (var i = 0; i < geometry.vertices.length; i++) {
//根据顶点的位置计算出骨骼影响下标和权重
var vertex = geometry.vertices[i];
var y = (vertex.y + sizing.halfHeight);
var skinIndex = Math.floor(y / sizing.segmentHeight);
var skinWeight = (y % sizing.segmentHeight) / sizing.segmentHeight;
geometry.skinIndices.push(new THREE.Vector4(skinIndex, skinIndex + 1, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - skinWeight, skinWeight, 0, 0));
}
几何体的 skinIndices 属性和 skinWeights 属性分别用来设置绑定的骨骼下标和权重(骨骼影响程度)。
相应的,我们需要一组相关联的骨骼。骨骼具有嵌套关系,才得以实现一个骨架。圆柱体比较简单,我们直接创建一条骨骼垂直嵌套的骨骼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bones = [];
var prevBone = new THREE.Bone();
bones.push(prevBone);
prevBone.position.y = -sizing.halfHeight;
for (var i = 0; i < sizing.segmentCount; i++) {
var bone = new THREE.Bone();
bone.position.y = sizing.segmentHeight;
bones.push(bone); //添加到骨骼数组
prevBone.add(bone); //上一个骨骼定义为父级
prevBone = bone;
}
创建纹理时,我们还需要设置当前材质属性,并开启骨骼动画对其的修改权限,将材质的 skinning 属性设置为 true:
1
2
3
4
var lineMaterial = new THREE.MeshBasicMaterial({
skinning: true,
wireframe: true
});
最后,我们需要创建骨骼材质,并将模型绑定骨骼:
1
2
3
4
mesh = new THREE.SkinnedMesh(geometry, [material, lineMaterial]);
var skeleton = new THREE.Skeleton(bones); //创建骨架
mesh.add(bones[0]); //将骨骼添加到模型里面
mesh.bind(skeleton); //模型绑定骨架
这样,我们就使用 Three.js 创建了一个简单的骨骼动画。使用 dat.gui
,便于我们修改每一个骨骼的 poisition、rotation 和 scale 并查看对当前模型的影响。
案例的源码地址:点击这里。
两种动画的区别
变形动画主要用于精度要求高的动画,比如人物的面部表情。其优点是动画的展现效果很到位,缺点就是扩展性不强,只能执行设置好的相关动画。
骨骼动画主要用于精度要求相对低一些,但需要丰富多样的动画的场合,就比如人物的走动,攻击防御等动画,我们可以通过一套骨骼,修改相应骨骼的位置信息直接实现相应的效果。它没有变形动画的精度高,但可以实现多种多样的效果。
总结: 我们可以根据项目的需求来设置不同的动画,就比如一个人物模型,说话我们使用变形动画去实现,而肢体动作使用骨骼动画去实现。
导入模型动画
在 Three.js 动画系统中,你可以为模型的各种属性设置动画,如骨骼动画,变形动画,以及材质的相关属性(颜色,透明度, 是否可见)。动画属性可以设置淡入淡出效果以及各种扭曲特效,也可以单独改变一个或多个对象上的动画影响程度和动画时间。
为了实现这些,Three.js 动画系统在2015年修改为了类似于 Unity 和虚幻引擎4的架构。接下来我们了解下这套动画系统的主要组件以及它们是如何协同工作的。
动画片段(Animation Clips)
在我们成功导入模型以后,如果模型拥有相关的动画属性,会在返回的模型数据中产生一个名为 animations 的数组,数组的每一个子项都是一个 AnimationClips 对象。
每一个单独 AnimationClips 对象相应的保存着模型的一个动画的数据,假如,如果模型网格是一个人物角色,第一个 AnimationClips 对象有可能保存的是人物走动的动画,第二个 AnimationClips 对象用于跳跃,第三个用于攻击动画等等。
关键帧轨迹(Keyframe Tracks)
在 AnimationClips 对象内部,一般有四个属性:
- name:当前动画的名称;
- uuid:一个不会重复的 uuid;
- duration:当前动画一个循环所需要的时间;
- tracks:轨迹,即当前动画每一次切换动作所需要的数据。
假设当前的动画是骨骼动画,在关键帧轨迹中存储的数据是每一帧骨骼随着时间变动的数据(位置,旋转和缩放等)。
如果当前动画是一个变形动画,在关键帧轨迹中将会把顶点数据的变动存储在其中(比如实现人脸的笑以及哭等动作)。
动画混合器(Animation Mixer)
在动画片段中存储的数据仅仅构成了动画实现的基础,实际的播放权力在动画混合器的手中。你可以想象动画混合器其实不仅仅只是作为动画的播放器,它还可以同时控制几个动画,混合它们或者合并它们。
动画播放器(Animation Actions)
这个英文我更乐意将它翻译成动画播放器,因为我们最终需要将数据生成一个动画播放器来操作当前的动画执行,暂停或者停止,是否使用淡入淡出效果或者将动画加快或减慢。
动画对象组(Animation Object Groups)
如果你希望一组模型对象共享当前的动画,我们可以使用动画对象组来实现。
通过导入模型显示动画

变形动画
我们首先查看一个官方的模型案例,这个案例是一匹马奔跑的动画,我们也可以通过下面地址查看:点击这里。
接下来我们看一下这匹马是如何实现的。
- 在模型加载成功以后,我们首先将模型创建出来,并将材质的 morphTargets 设置为 ture,使顶点数据信息可以受变形动画影响:
1
2
3
4
5
6
7
mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({
vertexColors: THREE.FaceColors,
morphTargets: true
}));
mesh.castShadow = true;
mesh.scale.set(0.1, 0.1, 0.1);
scene.add(mesh);
- 然后我们创建了一个针对于该模型的混合器:
1
mixer = new THREE.AnimationMixer(mesh);
- 接着使用变形目标数据创建一个动画片段:
1
var clip = THREE.AnimationClip.CreateFromMorphTargetSequence('gallop', geometry.morphTargets, 30);
- 使用混合器和动画片段创建一个动画播放器来播放:
1
2
3
var action = mixer.clipAction(clip); //创建动画播放器
action.setDuration(1); //设置当前动画一秒为一个周期
action.play(); //设置当前动画播放
- 最后,我们还需要在重新绘制循环中更新混合器,进行动作更新:
1
2
3
4
5
6
7
8
9
10
11
12
function render() {
control.update();
var time = clock.getDelta();
//由于模型导入是异步的,所以我们再模型没有加载完之前是获取不到混合器的
if (mixer) {
mixer.update(time);
}
renderer.render(scene, camera);
}

骨骼动画
骨骼动画模型,我们使用的是 gltf 格式,这个模型是在 Sketchfab 网站下载的,案例是一个小姐姐跳舞的片段,查看地址:点击这里。
gltf 格式的模型导入进来后,我们可以直接通过 animations 数组创建播放器:
1
2
mixer = new THREE.AnimationMixer(obj); //通过当前模型创建混合器
action = mixer.clipAction(gltf.animations[0]); //通过动画数据创建播放器
直接调用播放器的播放事件让动画播放:
1
action.play();
最后,我们还需要在循环渲染中更新混合器,并将每一帧渲染的间隔时间传入:
1
2
3
4
5
6
7
8
function render() {
control.update();
var time = clock.getDelta();
if (mixer) {
mixer.update(time);
}
renderer.render(scene, camera);
}
Tween.js 补间动画
Tween 是什么
Tween.js 是 JavaScript 中一个简单的补间动画库,包含各种经典动画算法。Tween.js 支持数字对象的属性和 CSS 样式属性赋值,API 简单且强大,支持链式调用。
补间(动画)(来自 In-Between)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这个过程需要多长时间,补间引擎将负责计算从起始点到结束点的值。
在 Three.js 中,我们有一些修改模型位置,旋转和缩放的需求,却无法直接在 WebGL 中使用 CSS3 动画来实现,而 Tween.js 恰好给我们提供了一个很好的解决方案。
比如我们要实现一个模型从 A 点到 B 点的位置移动,常规的实现方法,是使用 setInterval、requestAnimationFrame 手动计算出特定时间的位置点,很不易于管理与查看。而 Tween.js 可以自动根据起始点位置和动画时长计算出所有的位置点,可以很方便地对其进行获取和管理。
简单应用
接下来,我们通过一个案例,带大家了解如何在 Three.js 应用中使用 Tween.js。
案例 Demo 查看地址:点击这里。
案例代码查看地址:点击这里。
本案例的开发思路是:首先获取目标模型的初始位置,然后实例化 Tween,接着设置目标位置,启动 Tween,在 TWEEN.onUpdate() 回调中改变目标模型的位置,从而实现目标模型从初始位置平滑移动到目标位置的动画。
实现代码如下:
1
2
3
4
5
6
7
8
9
//设置tween
var position = {x:-40, y:0, z:-30};
tween = new TWEEN.Tween(position);
//设置移动的目标和移动时间
tween.to({x:40, y:30, z:30}, 2000);
//设置每次更新的回调,然后修改几何体的位置
tween.onUpdate(function (pos) {
cube.position.set(pos.x, pos.y, pos.z);
});
上面代码,首先创建一个 position 对象,存储了当前立方体的位置数据。然后,通过当前的对象创建了一个补间 tween。紧接着,设置每一个属性的目标位置,并告诉 Tween 在 2000 毫秒(动画时长)内移动到目标位置。最后,设置 Tween 对象每次更新的回调,即在每次数据更新以后,将立方体位置更新。
Tween 对象不会直接执行,需要我们调用 start()
方法激活,即 tween.start()
。
1
2
3
4
5
6
//声明一个保存需求修改的数据对象。
gui = {
start:function () {
tween.start();
}
};
想要完成整个过程,我们还需要在每帧里面调用 TWEEN.update
,来触发 Tween 对象更新位置:
1
2
3
4
5
6
7
8
9
function render() {
//更新Tween
TWEEN.update();
control.update();
renderer.render(scene, camera);
}
链式调用
链式调用可以简化大量代码,逻辑清晰集中,便于查看和修改。
Tween 插件也支持链式调用的方法,并且还会修改实例化时传入的对象,如下代码:
1
2
3
4
5
6
7
8
9
10
11
//设置tween
var position = {x:-40, y:0, z:-30};
tween = new TWEEN.Tween(position);
//设置移动的目标和移动时间
tween.to({x:40, y:30, z:30}, 2000);
//设置每次更新的回调,然后修改几何体的位置
tween.onUpdate(function (pos) {
cube.position.set(pos.x, pos.y, pos.z);
});
可以简化为链式调用:
1
2
//直接链式实现tween
tween = new TWEEN.Tween(cube.position).to({x:40, y:30, z:30}, 2000);
Tween 对象方法
控制动画方法
Tween 对象控制动画的方法主要包括开始、取消、重复等方法。
.start()
如果你想激活一个补间,请使用这个方法,调用方式如下所示:
1
tween.start();
start()
方法还接受一个时间参数,添加该参数后,补间不会立即被激活,Tween 动画将在延时该时间数后才开始动画。否则它将立刻开始动画,且在第一次调用 TWEEN.update
时开始计时。如果设置的时间已经小于计时的总时间,那计算出来的位置数据将是参数设置时间开始后,运行到的所在位置。
.stop()
这个方法刚好和 start()
方法对应,如果你想取消一个补间,直接调用这个方法即可:
1
tween.stop();
.update()
其实每个补间都有一个更新方法,只不过我们多会使用 TWEEN.update
,而不会单独调用它(见下方全局函数)。
.chain()
当你按顺序排列不同的补间时,例如当上一个补间结束时立即启动另外一个补间,我们称它为链式补间。关于链式补间的案例请见下面两个链接。
案例 Demo 查看地址:点击这里
案例代码查看地址:点击这里
调用方法为:
1
tweenA.chain(tweenB);
或者,采用一个无限的链式,即 tweenA 与 tweenB 无限循环,便可以写成:
1
2
tweenA.chain(tweenB);
tweenB.chain(tweenA);
在其他情况下,您可能需要将多个补间链接到另一个补间,以使它们(链接的补间)同时开始动画:
1
tweenA.chain(tweenB,tweenC);
警告:调用 tweenA.chain(tweenB)
实际上修改的是 tweenA,tweenB 总在 tweenA 完成时启动。chain()
的返回值只是 tweenA,不是一个新的 Tween。
链接多个补间时,比如 tweenA.chain(tweenB, tweenC)
表示 tweenA 动画结束后,tweenB 和 tweenC 动画同时开始,如果因 tweenB 和 tweenC 修改的属性相同,而存在冲突时,经测试写在后面的属性将是最终动画位置。
注意,一般不要让两个同时开始的补间存在属性冲突。
.repeat()
如果想让一个补间永远重复,可以无限链接自己,但更好的方法是使用 repeat()
方法。它接受一个参数,描述第一个补间完成后需要重复多少次,如下代码示例:
1
2
tween.repeat(10); // 循环10次
tween.repeat(Infinity); // 无限循环
我们可以将案例中 simple.html
文件里面的调用改成无限循环,代码如下:
1
tween = new TWEEN.Tween(cube.position).to({x:40, y:30, z:30}, 2000).repeat(Infinity);
.yoyo()
该方法只有在补间使用 repeat()
方法时才会被调用。我们调用 yoyo()
以后,位置的切换就变成了从头到尾,从尾到头这样的循环过程。
单个补间无限从头到尾的循环,可以写成这样:
1
tween = new TWEEN.Tween(cube.position).to({x:40, y:30, z:30}, 2000).repeat(Infinity).yoyo(true);
进一步了解 yoyo()
方法的使用,可查看下面案例。
案例 Demo 查看地址:点击这里。
案例代码查看地址:点击这里。
.delay()
这个方法用于控制激活前的延时,即触发 start()
事件后,需要延时到设置的 delay 时间,才会真正激活,使用方法见下面代码所示:
1
2
tween.delay(1000);
tween.start();
回调函数
Tween 每次的位置更新后,都会触发 onUpdate 回调函数,我们可以在此回调中修改模型位置。
在之前案例的 simple.html
中,我们在每次更新回调中获取更新后的位置信息并修改模型几何体的位置:
1
2
3
4
//设置每次更新的回调,然后修改几何体的位置
tween.onUpdate(function (pos) {
cube.position.set(pos.x, pos.y, pos.z);
});
目前,补间支持的回调函数主要有以下几种。
- onStart
在补间计算开始前的回调,每个补间只能触发一下,即使使用 repeat()
方法循环,这个回调也只被触发一次。
- onStop
通过调用 stop()
方法停止的补间会触发当前回调,如果是正常完成的补间将不会触发此回调。
- onUpdate
每次补间更新后,我们可以在此回调中获取更新后的值。
- onComplete
当补间正常完成时,将会触发此回调。通过使用 stop()
停止的补间将不会触发此回调。
总结
在 Three.js 中使用 Tween.js,能够很方便地改变模型的位置,不需要手动计算,便能获取到具体的位置数据,以实现我们的需求。
Three.js 场景交互
浏览器是一个 2D 视口,而 Three.js 展示的是 3D 场景。场景交互时,需要在二维平面中控制三维场景的模型,那如何将 2D 视口的 x 和 y 坐标转换成 Three.js 场景中的 3D 坐标呢?
好在 Three.js 已经有了解决相关问题的方案,那就是 THREE.Raycaster
射线,用于鼠标拾取(计算出鼠标移过的三维空间中的对象)等。我们看下面这张图片:

我们一般都会设置三维场景的显示区域,如果指明当前显示的 2D 坐标给 THREE.Raycaster
,它将生成一条从显示起点到终点的射线,也就是射线与近视面交点和射线与远视面交点连成的这一条直线。在相机视角下查看,只是一个点,射线会穿过整个显示场景,并按从近到远的顺序返回与模型相交的数据。
THREE.Raycaster 构造函数和对象方法
实例化
1
new THREE.Raycaster( origin, direction, near, far );
该实例化函数 Raycaster 包含了四个参数。
- origin:光线投射的原点矢量;
- direction:光线投射的方向矢量,应该是被归一化的;
- near:投射近点,用来限定返回比 near 要远的结果。near 不能为负数,缺省为 0;
- far:投射远点,用来限定返回比 far 要近的结果。far 不能比 near 小,缺省为无穷大。
属性
THREE.Raycaster
的属性可以在实例化对象后有修改需求时再修改。除了上面提到的 origin、direction、near、far 四个属性外,我们还有可能用到另一个属性:
- linePrecision:射线和线相交的精度,浮点数类型的值。
方法
THREE.Raycaster
给我们提供了一系列的方法,比如修改射线的位置,判断与某些模型是否相交等,接下来我们列举一些经常使用的方法。
.set()
:此方法可以重新设置射线的原点和方向,从而更新射线位置。
1
.set(origin,direction)
其中,参数 origin 用来设置射线新的原点矢量位置,direction 用来设置基于原点位置的射线的方向矢量。
.setFromCamera()
:使用当前相机和界面的 2D 坐标设置射线的位置和方向。
1
.setFromCamera ( coords, camera )
参数 coords 表示鼠标的二维坐标,在归一化的设备坐标(NDC)中,也就是 X 和 Y 分量,应该介于 -1 和 1 之间。camera 表示射线起点处的相机,即把射线起点设置在该相机位置处。
点击事件大多通过鼠标触发,我们用鼠标点击显示区域的位置和当前场景使用的相机对象调用此对象,Three.js 会为我们计算出当前射线的位置。
.intersectObject ()
和 .intersectObjects ()
两个方法用来检查射线和物体之间的所有交叉点数据。
如果检测射线和一个对象是否相交,推荐使用 intersectObject()
,如果判断的是这个对象的子对象,那推荐使用 intersectObjects()
,将 3D 对象的 children 属性传入。
返回值是一个交叉点对象数组,且按距离排序,最接近的排在首位。
1
.intersectObject ( object, recursive, optionalTarget)
参数 object,用来检测和射线相交的物体。如果 recursive 设置为 true,还会向下继续检查所有后代,否则只检查该对象本身,缺省值为 false。optionalTarget 为可选参数,用于设置放置结果的数组,如果缺省,则将会实例化一个新数组,并将获取到的数据放入其中。
1
.intersectObjects ( array, recursive, optionalTarget)
intersectObject()
和 intersectObjects()
的区别在于第一个参数。intersectObject 的第一个参数为 3D 对象,而 intersectObjects 需要传入一个由 3D 对象组成的数组。
我们知道两个方法的返回值均为对象数组。接下来,我们再进一步了解下这个返回值。
如果射线与场景内的模型没有相交,将返回一个空数组,否则,将返回一个按从近到远顺序排列的对象数组,数组中每个对象的内容为:
1
[ { distance, point, face, faceIndex, indices, object }, ... ]
其中:
- distance:射线的起点到相交点的距离;
- point:在世界坐标中的交叉点;
- face:相交的面;
- faceIndex:相交的面的索引;
- indices:组成相交面的顶点索引;
- object:相交的对象。
当一个网孔(Mesh)对象和一个缓存几何模型(BufferGeometry)相交时,faceIndex 将是 undefined,并且 indices 将被设置;而当一个网孔(Mesh)对象和一个几何模型(Geometry)相交时,indices 将是 undefined。
当计算这个对象是否和射线相交时,Raycaster 把传递的对象委托给检测 3D 对象的 raycast 方法,该方法通过计算,检测出当前模型与射线是否相交。
这允许 Mesh 对光线投射的反应可以不同于 lines 和 pointclouds。Mesh、lines、pointclouds 是三种不同的计算相交的方法。Mesh 对射线与网格对象的每一个面进行相交判断。lines,则是判断两条线之间的距离,至于 pointclouds,则是通过 distanceSqToPoint 判断当前是否相交。
注意,对于网格,面(faces)必须朝向射线原点,这样才能被检测到。背面射线的交叉点将无法被检测到。
为了使光线能投射到一个对象的正反两面,你需要设置 material 的 side 属性为 THREE.DoubleSide。
模型点击事件的实现
上面讲解了射线的相关内容,接下来,我们来看一下,如何使用射线实现一个普通的点击事件。
首先,我们通过点击事件回调的 event 获取点击的位置:
1
2
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
默认没有经过矩阵转换过的显示区域的宽和高分别是 2,即中心点也是 WebGL 场景的坐标原点,左上角的坐标是 (-1.0, 1.0, 0.0)
,右下角的坐标是 (1.0, -1.0, 0.0)
。我们通过单击点的位置计算出当前该点在场景中,没有被矩阵转换过的平面坐标。如果 WebGL 的渲染区域没有占满窗口,我们还需获取显示区域距离窗口左上角的偏移量,再计算位置,计算方法如下:
1
2
3
4
5
6
7
8
9
10
11
//通过 dom 的 getBoundingClientRect 方法获得当前显示区域距离左上角的偏移量
var left = renderer.domElement.getBoundingClientRect().left;
var top = renderer.domElement.getBoundingClientRect().top;
//根据浏览器的设备类型来获取到当前点击的位置
var clientX = dop.browserRedirect() === "pc" ? event.clientX - left : event.touches[0].clientX - left;
var clientY = dop.browserRedirect() === "pc" ? event.clientY - top : event.touches[0].clientY - top;
//计算出场景内的原始坐标
mouse.x = (clientX / renderer.domElement.offsetWidth) * 2 - 1;
mouse.y = -(clientY / renderer.domElement.offsetHeight) * 2 + 1;
获取到坐标以后,我们需要使用射线的 setFromCamera()
方法配合场景坐标和相机更新射线的位置:
1
raycaster.setFromCamera( mouse, camera );
接着,使用 intersectObjects()
方法获取射线和所有模型相交的数组集合:
1
var intersects = raycaster.intersectObjects( scene.children );
这里再提醒一句,很多读者可能发现,有时点击后射线并未获取到相交的物体。这是因为我们一般使用 intersectObject()
和 intersectObjects()
时,只会传入对象,而有的模型由多个模型组成,也就是它的子类,这时我们需要传入第二个值,设置为 true,来提示 Three.js 遍历它的子类。
最后,如果有与射线相交的模型,返回的 intersects 数组的长度将不为零:
1
2
3
if(intersects.length > 0){
alert("有相交的模型");
}
这里提供一个点击案例,点中物体后,模型颜色将会变色:点击这里 。
案例源码地址:点击这里。
简单框选案例的实现
最近有些小伙伴想实现一个简单的框选案例,在这一篇中我来带大家完成。
本案例通过判断模型的位置实现框选,即框选前先获取所有模型在二维平面上的位置,然后再判断这些二维平面上的点是否处于框内。
相对于其它实现方式,这种实现节约性能,简单易懂,能够应付大部分场景。
接下来,我讲解一下这个框选的实现思路。
首先,编写以下代码,当鼠标按下时,记录鼠标按下时的场景坐标:
1
2
3
4
5
6
//获取到显示区域距离窗口左上角的偏移量
domClient.x = renderer.domElement.getBoundingClientRect().left;
domClient.y = renderer.domElement.getBoundingClientRect().top;
//计算出当前鼠标距离显示区域左上角的距离
down.x = e.clientX - domClient.x;
down.y = e.clientY - domClient.y;
前两行代码求出了当前显示区域距离窗口左上角的偏移量,后两行则计算出来了当前鼠标点击位置距离显示区域左上角的偏移。
接着,使用 box 对象方法计算出模型的包围盒中心位置,适用于多个复杂模型场景。如果只是简单几何体,可以直接使用 Mesh 的位置来计算。通过相机将世界坐标的位置转换为平面坐标,并将模型放到一个数组内以便后期使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (let i = 0; i < group.children.length; i++) {
let box = new THREE.Box3();
box.expandByObject(group.children[i]);
//获取到平面的坐标
let vec3 = new THREE.Vector3();
box.getCenter(vec3);
let vec = vec3.project(camera);
modelsList.push(
{
component: group.children[i],
position: {
x: vec.x * half.width + half.width,
y: -vec.y * half.height + half.height
},
normalMaterial: group.children[i].material
}
)
}
上面代码首先通过一个循环,计算出了每一模型在二维平面中的位置。
接下来,绑定 Document 的 mousemove 事件和 mouseup 事件。鼠标移动事件用来判断每个模型是否处于框内,鼠标抬起事件则将绑定的事件清除。
1
2
3
//绑定鼠标按下移动事件和抬起事件
document.addEventListener("mousemove", movefun, false);
document.addEventListener("mouseup", upfun, false);
在鼠标移动事件中,我们计算出当前四个边的位置,并且循环判断哪些模型的位置处于框内,处于框内的模型的材质将被修改为框选材质:
1
2
3
4
5
6
7
8
9
10
for (let i = 0; i < modelsList.length; i++) {
let position = modelsList[i].position;
//判断当前位置是否处于框内
if (position.x > min.x && position.x < max.x && position.y > min.y && position.y < max.y) {
modelsList[i].component.material = material;
}
else{
modelsList[i].component.material = modelsList[i].normalMaterial;
}
}
在最后的鼠标抬起事件内,将框选框隐藏,并将所有材质修改为默认材质:
1
2
3
4
5
6
7
8
9
10
11
12
function upfun(e) {
//清除事件
document.body.removeChild(div);
document.removeEventListener("mousemove", movefun, false);
document.removeEventListener("mouseup", upfun, false);
//将所有的模型修改为当前默认的材质
for (let i = 0; i < modelsList.length; i++) {
modelsList[i].component.material = modelsList[i].normalMaterial;
}
}
最后,附上可以查看的案例地址:点击这里。
案例源码地址:点击这里
Three.js 性能优化
在接触 Three.js 一段时间后,或多或少都会遇到性能问题。此类问题将直接导致页面帧率过低,严重时会导致页面崩溃。
导致性能问题的原因有很多,比如模型文件太大或顶点数太多导致加载时间过长,甚至失败,又或者代码书写逻辑有问题导致场景帧数太低。面对不同原因,我们需采取不同的应对方案,比如面对模型问题可对模型进行减面、减体积等处理;针对代码问题,则需尽量减少代码的运算。在平时的开发中,我们还要尽可能避免导致这些性能问题的根由。
下面,我将分享几种简单的有助于提升性能的方法。
尽量共用几何体和材质
当大批量进行模型渲染时,不可避免地会有大量重复几何体的创建,这时共用相同的几何体和材质可以减少内存和 GPU 的数据传输。
我们具体来看一个实例,比如你需要创建三百个简单的相同颜色的立方体模型,普通的实现方法如下:
1
2
3
4
5
6
7
8
for (let i = 0; i < 300; i++) {
let geometry = new THREE.BoxGeometry(10, 10, 10);
let material = new THREE.MeshLambertMaterial({color: 0x00ffff});
let mesh = new THREE.Mesh(geometry, material);
//随机位置
mesh.position.set(THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200));
group.add(mesh);
}
上面代码创建了三百个相同的几何体和材质,该方法中有很多不必要的创建过程,增加运算的同时,还浪费了内存。
要解决这些性能问题,创建时我们可以共用相同的几何体和材质,改良后的代码如下所示:
1
2
3
4
5
6
7
8
let geometry = new THREE.BoxGeometry(10, 10, 10);
let material = new THREE.MeshLambertMaterial({color: 0x00ffff});
for (let i = 0; i < 300; i++) {
let mesh = new THREE.Mesh(geometry, material);
//随机位置
mesh.position.set(THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200), THREE.Math.randFloatSpread(200));
group.add(mesh);
}
利用上面代码,我们只需创建一套相同的几何体的顶点数据,不仅降低了内存消耗,还提高了添加运算效率。
模型删除时,材质和几何体也需从内存中清除
我们使用 remove()
将模型从场景内删除后,发现内存占用并没有太大变化。这是因为几何体和材质还保存在内存当中,这时需要手动调用 dispose()
方法将其从内存中删除。
下面为删除整个场景组的案例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//删除group
function deleteGroup(name) {
let group = scene.getObjectByName(name);
if (!group) return;
//删除掉所有的模型组内的mesh
group.traverse(function (item) {
if (item instanceof THREE.Mesh) {
item.geometry.dispose(); //删除几何体
item.material.dispose(); //删除材质
}
});
scene.remove(group);
}
使用 merge 方法合并不需要单独操作的模型
在 Three.js 新版本中,将 merge 方法整合在了几何体上,主要应用于拥有大量几何体且材质相同的模型上。我们可以将多个几何体拼接成单个整体的几何体,从而达到节约性能的目的,但该做法的缺点则是失去了对单个模型的控制。
通过下面代码,我们了解下 merge 的使用方法:
1
2
3
4
5
6
7
8
9
10
11
//合并模型,则使用merge方法合并
var geometry = new THREE.Geometry();
//merge方法将两个几何体对象或者Object3D里面的几何体对象合并,(使用对象的变换)将几何体的顶点、面、UV 分别合并。
//THREE.GeometryUtils: .merge() has been moved to Geometry. Use geometry.merge( geometry2, matrix, materialIndexOffset ) instead. 如果新版本用老版本的会报这个错
for(var i=0; i<20000; i++){
var cube = addCube(); //创建了一个随机位置的几何体模型
cube.updateMatrix(); //手动更新模型的矩阵
geometry.merge(cube.geometry, cube.matrix); //将几何体合并
}
scene.add(new THREE.Mesh(geometry, cubeMaterial));
我们再看一个案例:点击这里。
在上面案例中,在不选中 combined 的前提下 ,选择 redraw 20000 个模型的话,一般只有十几帧的帧率。但如果选中了 combined,会发现渲染的帧率可达到满帧(60帧),性能得到了巨大提升。
在循环渲染中避免使用更新
几何体、材质、纹理等相关数据的更新尽量不要放到循环渲染中进行。循环渲染时,每一帧都会对 GPU 数据进行更新,这将引发很多不必要的数据更新过程。
接下来,我们了解下几何体、材质、纹理数据更新的相关属性。
- 几何体
1
2
3
4
5
6
geometry.verticesNeedUpdate = true; //顶点发生了修改
geometry.elementsNeedUpdate = true; //面发生了修改
geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
geometry.uvsNeedUpdate = true; //uv映射发生了修改
geometry.normalsNeedUpdate = true; //法向发生了修改
geometry.colorsNeedUpdate = true; //顶点颜色发生的修改
- 材质
1
material.needsUpdate = true;
- 纹理
1
texture.needsUpdate = true;
如果它们发生更新,则将其设置为 true,Three.js 会通过判断,将数据重新传输到显存当中,并将配置项重新修改为 false。这是一个十分影响运行效率的过程,我们尽量只在需要的时候修改,且不要放到 render()
方法当中循环设置。
只在需要的时候渲染
在没有操作的时候,一直循环渲染属于浪费资源。接下来我带给大家一个只在需要时渲染的方法。
首先在循环渲染中加入一个判断,如果判断值为 true 时,才可以循环渲染:
1
2
3
4
5
6
7
8
9
10
11
var renderEnabled;
function animate() {
if (renderEnabled) {
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
}
animate();
然后设置一个延迟器函数,每次调用后,可以将 renderEnabled 设置为 true,并延迟三秒将其设置为 false,这个延迟时间大家可以根据需求来修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//调用一次可以渲染三秒
let timeOut = null;
function timeRender() {
//设置为可渲染状态
renderEnabled = true;
//清除上次的延迟器
if (timeOut) {
clearTimeout(timeOut);
}
timeOut = setTimeout(function () {
renderEnabled = false;
}, 3000);
}
接下来,我们在需要的时候调用这个 timeRender()
方法即可,比如在相机控制器更新后的回调中:
1
2
3
controls.addEventListener('change', function(){
timeRender();
});
如果相机位置发生变化,就会触发回调,开启循环渲染,更新页面显示。
如果我们添加了一个模型到场景中,直接调用重新渲染即可:
1
2
scene.add(mesh);
timeRender();
最后一个重点问题,就是材质的纹理为异步渲染,需要在图片添加完成后,触发回调。好在 Three.js 已经考虑到了这一点,Three.js 的静态对象 THREE.DefaultLoadingManager
的 onLoad 回调会在每一个纹理图片加载完成后触发回调。依靠它,我们可以在 Three.js 的每一个内容发生变更后触发重新渲染,且闲置状态下停止渲染。
1
2
3
4
//每次材质和纹理更新,触发重新渲染
THREE.DefaultLoadingManager.onLoad = function () {
timeRender();
};
Three.js 核心对象
本文是介绍 Three.js 核心技术知识的最后一节,带大家了解经常接触到的对象。第16课,也是课程的最后一节,我们就开始实战演练。
THREE.Clock
时间对象用于跟踪时间,用于计算每一帧的渲染时间或者从开始渲染到结束的时间。
构造函数
构造函数的使用,如下所示:
1
var clock = new THREE.Clock();
实例化时间对象,还可以接收一个布尔类型的参数,用于设置实例化完成后,是否启动时间对象计时,默认为开启。
方法
该类主要有以下几种方法。
- .start():此方法用于启动时间对象开始计时,将所有的信息重置;
- .stop():停止时间对象的计时;
- .getElapsedTime():此方法返回自时间对象开启以后到现在的时间;
- .getDelta():此方法返回上次调用到这次调用此方法的间隔时间,用于计算渲染间隔。
THREE.Color
THREE.Color 为颜色类,用到颜色的地方,都可以实例化此类,如设置材质颜色、场景背景颜色等。
构造函数
我们可以通过 new THREE.Color()
传入一个值来初始化一个颜色对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//不传参数将默认渲染成白色
var color = new THREE.Color();
//十六进制颜色(推荐)
var color = new THREE.Color( 0xff0000 );
//RGB 字符串
var color = new THREE.Color("rgb(255, 0, 0)");
var color = new THREE.Color("rgb(100%, 0%, 0%)");
//支持所有140个颜色名称
var color = new THREE.Color( 'skyblue' );
//HSL 字符串
var color = new THREE.Color("hsl(0, 100%, 50%)");
//支持区间为从0到1的RGB数值
var color = new THREE.Color( 1, 0, 0 );
方法
该类主要有以下几种方法。
- .clone():返回一个相同颜色的颜色对象;
- .add():将一个颜色对象的颜色值和此颜色对象的颜色相加;
- .multiply():将一个颜色对象的颜色值和此颜色对象的颜色相乘;
- .set():重新设置颜色对象的颜色,支持实例化时的所有方式。
矢量对象
矢量是 GLSL ES 语言内的标准数据类型,也是 WebGL 原生着色器语言的数据类型。而 Three.js 为了方便和原生融合,内置了 THREE.Vector2
、THREE.Vector3
和 THREE.Vector4
三种矢量,分别代表二维向量、三维向量和四维向量。
二维矢量
二维矢量一般表示二维平面上点的位置和方向。
下面代码生成了一个普通的二维矢量:
1
var a = new THREE.Vector2( 0, 1 );
我们也可以通过 set()
方法,重新修改二维矢量的值:
1
a.set(2, 3);
三维矢量
在项目当中,我们经常会用到三维矢量,比如模型的位置属性、缩放属性,都是一个三维矢量。它由三个有序的数字组成的。
使用三维矢量,我们可以表示:
- 三维空间中一个点的位置;
- 三维空间中两点连线的方向和长度;
- 任意有序的三个数字。
我们看下面这个例子:
1
2
3
4
5
6
7
8
//创建一个普通的三维矢量
var a = new THREE.Vector3( 0, 1, 0 );
//默认空值,将会重置为(0, 0, 0)
var b = new THREE.Vector3( );
//d矢量是从a点到b点的方向和长度
var d = a.distanceTo( b );
我们经常进行的模型位置改变以及缩放操作,其实都是通过修改三维矢量的值来实现的,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
//创建一个三维矢量
var vec = new THREE.Vector3(0, 1, 0);
//修改模型的位置
mesh.position = vec;
//修改三维矢量的值
vec.set(1, 2, 1);
//修改模型的缩放值
mesh.scale.set(2, 2, 2);
我们还可以修改三维向量的单个值:
1
2
3
4
5
vec.setComponent(0, 20); //将三维向量的第一值修改为20
.setComponent(index, value);
//index 修改的单个值的下标,可选值为 0 1 2 对应 vec.x vec.y vec.z
//value 修改成的数字
四维矢量
四维矢量是由四个数字组成的数据类型,四个值分别代表 x、y、z、w。
四维矢量可以表示的内容有:
- 思维空间内的一个点;
- 思维空间里的方向和长度;
- 任意有序的四个数字。
我们再看下面这个例子:
1
2
3
4
5
//创建一个普通的四维矢量
var a = new THREE.Vector4( 0, 1, 0, 0 );
//如果不设置参数,默认值为 (0, 0, 0, 1)
var b = new THREE.Vector4( );
四维矩阵
矩阵是高等代数中的常见工具,也常见于统计分析等应用数学学科中。Three.js 为我们封装了三维矩阵(3x3)四维矩阵(4x4)。
篇幅有限,这里就不介绍三维矩阵了,我们主要看下四维矩阵。
在 3D 计算机图形中,最常见的四维矩阵主要用于转换矩阵。这代表三维空间中的点 Vector3 通过乘以转换矩阵,可以实现如平移、旋转、剪切、缩放、发射、正交或透视投影等变化。
在每个 3D 对象中都有三个四维矩阵。
- Object3D.matrix:存储 3D 对象的局部变换,这是 3D 对象相对于父对象的转换;
- Object3D.matrixWorld:3D 对象的全局或世界变换,如果 3D 对象没有父对象,则当前矩阵和其局部变换矩阵相同。
- Object3D.modelViewMatrix:表示 3D 对象相对于摄像机的坐标转换,对象的 modelViewMatrix 是对象的 matrixWorld 再乘以相机的 matrixWorldInverse 获得的结果。
相机对象不但含有 3D 对象的四维矩阵,还额外拥有以下两个四维矩阵。
- Camera.matrixWorldInverse:视图矩阵(相机的 matrixWorld 反转后的四维矩阵);
- Camera.projectionMatrix:表示场景内的模型如何转换到二维显示区域的变换矩阵。
如果你对矩阵的实现原理感兴趣的话,可点击这里查看,这里不再过多解释原理。
接下来我们查看一下 Three.js 封装的四维变换矩阵为我们提供了哪些方法。
1
2
3
4
5
6
7
8
9
10
11
12
var m = new THREE.Matrix4();
//我们可以通过手动设置每一项的数值来生成变换矩阵
m.set( 11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44 );
//创建一个矢量,并应用此变换矩阵
var v = new THREE.Vector3();
v.applyMatrix4(m); //应用变换矩阵
上面代码创建了一个默认的四维矩阵,三维矢量乘以默认的四维矩阵将不会产生变化。
我们可以使用 .identity()
方法来重置变换矩阵:
1
2
var m = new THREE.Matrix4();
m.identity(); //重置矩阵恢复默认
我们可以通过 .lookAt()
传入一个三个矢量生成一个旋转矩阵。这三个矢量分别代表眼的位置、查看的物体位置和向上的方向:
1
2
var m = new THREE.Matrix4();
m.lookAt(eye, center, up); //生成旋转变换矩阵
我们也可以通过旋转弧度来生成旋转变换矩阵:
1
2
3
4
5
6
7
8
9
10
11
12
var m = new THREE.Matrix4();
//通过绕x轴旋转生成变换矩阵
m.makeRotationX(Math.PI/2); //绕x轴旋转90度变换矩阵
m.makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI/2); //效果同上
//通过绕y轴旋转生成变换矩阵
m.makeRotationY(Math.PI); //绕y轴旋转180度变换矩阵
m.makeRotationAxis(new THREE.Vector3(0, 1, 0), Math.PI); //效果同上
//通过绕z轴旋转生成变换矩阵
m.makeRotationZ(Math.PI/4); //绕z轴旋转45度变换矩阵
m.makeRotationAxis(new THREE.Vector3(0, 0, 1), Math.PI/4); //效果同上
通过设置缩放来生成缩放变换矩阵:
1
2
3
var m = new THREE.Matrix4();
m.makeScale(1, 2, 5); //生成沿x轴不变,y轴放大两倍,z轴放大五倍的缩放变换矩阵
通过设置位置平移来生成变换矩阵:
1
2
3
var m = new THREE.Matrix4();
m.makeTranslation(10, -20, 100); //生成沿x正轴偏移10,y负轴偏移20,z正轴偏移100的平移变换矩阵
欧拉角
欧拉角通过在每个轴指定旋转的弧度和指定旋转轴的先后顺序来进行旋转变换。之前介绍场景时已做介绍,不再赘述。这里想说明的是,每个 3D 对象的 rotation 属性都是一个欧拉角对象。
创建欧拉角定义方法如下:
1
var e = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
前三个值分别代表每个轴上旋转的弧度,第四个值是应用旋转的顺序。默认为“XYZ”,这意味着对象将首先围绕其 X 轴旋转,然后围绕其 Y 轴旋转,最后围绕其 Z 轴旋转。其他可能性有 YZX、ZXY、XZY、YXZ、ZYX,都必须大写。
修改欧拉角的方法如下:
1
2
3
//通过set方法重新设置
var e = new THREE.Euler();
e.set(Math.PI, 0, - Math.PI / 2, "YZX"); //先沿y轴旋转180度,再沿z轴旋转0度,最后沿x轴逆时针旋转90度,第四个值可以不填,默认是"XYZ"
我们也可以通过变换矩阵来修改当前的欧拉角:
1
2
3
4
var e = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
var m = new THREE.Matrix4();
e.setFromRotationMatrix(m); //在当前的旋转角度,再进行矩阵变换
结语
Three.js 知识讲解部分,到这里基本就结束了。第一次写正式的教程,难免有一些瑕疵。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/ab5e7d01.html b/posts/ab5e7d01.html
index 1f3adb52..3452d3df 100644
--- a/posts/ab5e7d01.html
+++ b/posts/ab5e7d01.html
@@ -1,4 +1,4 @@
-着色器之书07 - AIGISSS 着色器之书07
本文最后更新于:1 个月前
形状

终于!我们一直学习的技能就等着这一刻!你已经学习过GLSL的大部分基础,类型和函数。你一遍又一遍的练习你的造型方程。是时候把他们整合起来了。你就是为了这个挑战而来的!在这一章里,你会学习到如何以一种并行处理方式来画简单的图形。
长方形
想象我们有张数学课上使用的方格纸,而我们的作业是画一个正方形。纸的大小是10 x 10而正方形应该是8 x 8。你会怎么做?

你是不是会涂满除了第一行第一列和最后一行和最后一列的所有格点?
这和着色器有什么关系?方格纸上的每个小方形格点就是一个线程(一个像素)。每个格点有它的位置,就想棋盘上的坐标一样。在之前的章节我们将x和y映射到rgb通道,并且我们学习了如何将二维边界限制在0和1之间。我们如何用这些来画一个中心点位于屏幕中心的正方形?
我们从空间角度来判别的 if 语句伪代码开始。这个原理和我们思考方格纸的策略异曲同工。
1
2
3
4
if ( (X GREATER THAN 1) AND (Y GREATER THAN 1) )
paint white
else
paint black
现在我们有个更好的主意让这个想法实现,来试试把if语句换成step(),并用0到1代替10 x 10的范围。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uniform vec2 u_resolution;
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(0.0);
// Each result will return 1.0 (white) or 0.0 (black).
float left = step(0.1,st.x); // Similar to ( X greater than 0.1 )
float bottom = step(0.1,st.y); // Similar to ( Y greater than 0.1 )
// The multiplication of left*bottom will be similar to the logical AND.
color = vec3( left * bottom );
gl_FragColor = vec4(color,1.0);
}
step()函数会让没每一个小于0.1的像素变成黑色(vec3(0.0))并将其余的变成白色(vec3(1.0))。left
乘 bottom
效果相当于逻辑 AND —— 当 x y 都为 1.0 时乘积才能是 1.0。这样做的效果就是画了两条黑线,一个在画布的底边另一个在左边。

在前一例代码中我们重复每个像素的结构(左边和底边)。我们可以把原来的一个值换成两个值直接给step()来精减代码。就像这样:
1
2
vec2 borders = step(vec2(0.1),st);
float pct = borders.x * borders.y;
目前为止,我们只画了长方形的两条边(左边和底面)。看下下面的例子:
取消 21~22 行的注释来看看如何转置坐标的同时重复使用 step()
函数。这样二维向量 vec2(0.0,0.0) 会被变换到右上角。这就是转置页面和重复过程的数字等价。

注意在 18 行和 22 行,所有的边(宽)都被放大了。等价于这样写:
1
2
3
vec2 bl = step(vec2(0.1),st); // bottom-left
vec2 tr = step(vec2(0.1),1.0-st); // top-right
color = vec3(bl.x * bl.y * tr.x * tr.y);
是不是很有趣?这种都是关于运用 step() 函数、逻辑运算和转置坐标的结合。
再进行下一个环节之前,挑战下下面的练习:
改变长方形的比例和大小。
用 smoothstep() 函数代替 step() 函数,试试在相同的代码下会有什么不同。注意通过改变取值,你可以不仅可以得到模糊边界也可以由漂亮的顺滑边界。
应用 floor() 做个另外的案例。
挑个你最喜欢的做成函数,这样未来你可以调用它。并且让它灵活高效。
写一个只画长方形四边的函数。
想一下如何在一个画板上移动并放置不同的长方形?如果你做出来了,试着像Piet Mondrian一样创作以长方形和色彩的图画。

圆
在笛卡尔坐标系下,用方格纸来画正方形和长方形是很容易的。但是画圆就需要另一种方式了,尤其我们需要一个对“每个像素”的算法。一种解决办法是用step()
函数将重新映射的空间坐标来画圆。
如何实现?让我们重新回顾一下数学课上的方格纸:我们把圆规展开到半径的长度,把一个针脚戳在圆圆心上,旋转着把圆的边界留下来。

将这个过程翻译给 shader 意味着纸上的每个方形格点都会隐含着问每个像素(线程)是否在圆的区域以内。我们通过计算像素到中心的距离来实现(这个判断)。

There are several ways to calculate that distance. The easiest one uses the distance()
function, which internally computes the length()
of the difference between two points (in our case the pixel coordinate and the center of the canvas). The length()
function is nothing but a shortcut of the hypotenuse equation that uses square root (sqrt()
) internally.
有几种方法来计算距离。最简单的是用distance()
函数,这个函数其实内部调用 length()
函数,计算不同两点的距离(在此例中是像素坐标和画布中心的距离)。length()函数内部只不过是用平方根(sqrt()
)计算斜边的方程。

你可以使用distance()
, length()
或 sqrt()
到计算屏幕的中心的距离。下面的代码包含着三个函数,毫无悬念的他们返回相同的结果。
- 注释和取消某行的注释来试试看用不同方式得到相同的结果。
上回我们把到中心的距离映射为颜色亮度。离中心越近的越暗。注意到映射值不宜过高,因为从中心(vec2(0.5, 0.5))到最远距离才刚刚超过0.5一点。仔细考察这个映射:
你能从中推断出什么?
我们怎么用这个方法来画圆?
试试有没有其他方法来实现这样画布内圆形渐变的效果。
距离场
我们可也可以从另外的角度思考上面的例子:把它当做海拔地图(等高线图)——越黑的地方意味着海拔越高。想象下,你就在圆锥的顶端,那么这里的渐变就和圆锥的等高线图有些相似。到圆锥的水平距离是一个常数0.5。这个距离值在每个方向上都是相等的。通过选择从那里截取这个圆锥,你就会得到或大或小的圆纹面。

其实我们是通过“空间距离”来重新解释什么是图形。这种技巧被称之为“距离场”,从字体轮廓到3D图形被广泛应用。
来小试下牛刀:
用step()
函数把所有大于0.5的像素点变成白色,并把小于的变成黑色(0.0)。
反转前景色和背景色。
调戏下smoothstep()
函数,用不同的值来试着做出一个边界顺滑的圆。
一旦遇到令你满意的应用,把他写成一个函数,这样将来就可以调用了。
给这个圆来些缤纷的颜色吧!
再加点动画?一闪一闪亮晶晶?或者是砰砰跳动的心脏?(或许你可以从上一章汲取一些灵感)
让它动起来?能不能移动它并且在同一个屏幕上放置多个圆?
如果你结合函数来混合不同的距离场,会发生什么呢?
1
2
3
4
5
pct = distance(st,vec2(0.4)) + distance(st,vec2(0.6));
pct = distance(st,vec2(0.4)) * distance(st,vec2(0.6));
pct = min(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = max(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = pow(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
- 用这种技巧制作三个元素,如果它们是运动的,那就再好不过啦!
添加自己的工具箱
就计算效率而言,sqrt()
函数,以及所有依赖它的运算,都耗时耗力。dot()
点乘是另外一种用来高效计算圆形距离场的方式。
距离场的特点

距离场几乎可以用来画任何东西。显然,图形越复杂,方程也越复杂。但是一旦你找到某个特定图形的公式,就很容易添加图形或应用像过渡边界的效果。正因如此,距离场经常用于字体渲染,例如Mapbox GL Labels, Matt DesLauriers Material Design Fonts 和 as is describe on Chapter 7 of iPhone 3D Programming, O’Reilly.
看看下面的代码:
我们一开始把坐标系移到中心并把它映射到-1到1之间。在 24行 这儿,我们用一个fract()
函数来呈现这个距离场产生的图案。这个距离场不断重复,就像在禅花园看到的环一样。
现在我们来看下 19行 的距离场方程。这里我们在计算点 (.3,.3)
或 vec3(.3)
到所有四象限的距离(这就是 abs()
在起作用)。
如果你取消第 20行 的注释,你会发现我们把到四个点的距离用min()
函数合并到0,并产生了一个有趣的图案。
现在再试着取消第 21行 的注释,我们做的和之前一样,只不过这次用的是 max()
函数。这次的记过是圆角矩形。注意距离场的环形是如何离中心越远越光滑的。
最后从27 行到 29 行一行行地取消注释,思考距离场的不同用途。
极坐标下的图形

在关于颜色的章节我们通过如下的方程把每个像素的 半径 和 角度 笛卡尔坐标映射到极坐标。
1
2
3
vec2 pos = vec2(0.5)-st;
float r = length(pos)*2.0;
float a = atan(pos.y,pos.x);
我们用了部分方程在这章的开头来画圆,即用 length()
计算到中心的距离。现在我们可以用极坐标来画圆。
极坐标这种方式虽然有所限制但却十分简单。
下面你会看到在同样在笛卡尔坐标下图形在极坐标下的着色器案例(在 lines 21 和 25之间)。对这些函数一个个取消注释,看看两坐标系之间的联系。
着色器之书07 - AIGISSS 着色器之书07
本文最后更新于:1 个月前
形状

终于!我们一直学习的技能就等着这一刻!你已经学习过GLSL的大部分基础,类型和函数。你一遍又一遍的练习你的造型方程。是时候把他们整合起来了。你就是为了这个挑战而来的!在这一章里,你会学习到如何以一种并行处理方式来画简单的图形。
长方形
想象我们有张数学课上使用的方格纸,而我们的作业是画一个正方形。纸的大小是10 x 10而正方形应该是8 x 8。你会怎么做?

你是不是会涂满除了第一行第一列和最后一行和最后一列的所有格点?
这和着色器有什么关系?方格纸上的每个小方形格点就是一个线程(一个像素)。每个格点有它的位置,就想棋盘上的坐标一样。在之前的章节我们将x和y映射到rgb通道,并且我们学习了如何将二维边界限制在0和1之间。我们如何用这些来画一个中心点位于屏幕中心的正方形?
我们从空间角度来判别的 if 语句伪代码开始。这个原理和我们思考方格纸的策略异曲同工。
1
2
3
4
if ( (X GREATER THAN 1) AND (Y GREATER THAN 1) )
paint white
else
paint black
现在我们有个更好的主意让这个想法实现,来试试把if语句换成step(),并用0到1代替10 x 10的范围。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uniform vec2 u_resolution;
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(0.0);
// Each result will return 1.0 (white) or 0.0 (black).
float left = step(0.1,st.x); // Similar to ( X greater than 0.1 )
float bottom = step(0.1,st.y); // Similar to ( Y greater than 0.1 )
// The multiplication of left*bottom will be similar to the logical AND.
color = vec3( left * bottom );
gl_FragColor = vec4(color,1.0);
}
step()函数会让没每一个小于0.1的像素变成黑色(vec3(0.0))并将其余的变成白色(vec3(1.0))。left
乘 bottom
效果相当于逻辑 AND —— 当 x y 都为 1.0 时乘积才能是 1.0。这样做的效果就是画了两条黑线,一个在画布的底边另一个在左边。

在前一例代码中我们重复每个像素的结构(左边和底边)。我们可以把原来的一个值换成两个值直接给step()来精减代码。就像这样:
1
2
vec2 borders = step(vec2(0.1),st);
float pct = borders.x * borders.y;
目前为止,我们只画了长方形的两条边(左边和底面)。看下下面的例子:
取消 21~22 行的注释来看看如何转置坐标的同时重复使用 step()
函数。这样二维向量 vec2(0.0,0.0) 会被变换到右上角。这就是转置页面和重复过程的数字等价。

注意在 18 行和 22 行,所有的边(宽)都被放大了。等价于这样写:
1
2
3
vec2 bl = step(vec2(0.1),st); // bottom-left
vec2 tr = step(vec2(0.1),1.0-st); // top-right
color = vec3(bl.x * bl.y * tr.x * tr.y);
是不是很有趣?这种都是关于运用 step() 函数、逻辑运算和转置坐标的结合。
再进行下一个环节之前,挑战下下面的练习:
改变长方形的比例和大小。
用 smoothstep() 函数代替 step() 函数,试试在相同的代码下会有什么不同。注意通过改变取值,你可以不仅可以得到模糊边界也可以由漂亮的顺滑边界。
应用 floor() 做个另外的案例。
挑个你最喜欢的做成函数,这样未来你可以调用它。并且让它灵活高效。
写一个只画长方形四边的函数。
想一下如何在一个画板上移动并放置不同的长方形?如果你做出来了,试着像Piet Mondrian一样创作以长方形和色彩的图画。

圆
在笛卡尔坐标系下,用方格纸来画正方形和长方形是很容易的。但是画圆就需要另一种方式了,尤其我们需要一个对“每个像素”的算法。一种解决办法是用step()
函数将重新映射的空间坐标来画圆。
如何实现?让我们重新回顾一下数学课上的方格纸:我们把圆规展开到半径的长度,把一个针脚戳在圆圆心上,旋转着把圆的边界留下来。

将这个过程翻译给 shader 意味着纸上的每个方形格点都会隐含着问每个像素(线程)是否在圆的区域以内。我们通过计算像素到中心的距离来实现(这个判断)。

There are several ways to calculate that distance. The easiest one uses the distance()
function, which internally computes the length()
of the difference between two points (in our case the pixel coordinate and the center of the canvas). The length()
function is nothing but a shortcut of the hypotenuse equation that uses square root (sqrt()
) internally.
有几种方法来计算距离。最简单的是用distance()
函数,这个函数其实内部调用 length()
函数,计算不同两点的距离(在此例中是像素坐标和画布中心的距离)。length()函数内部只不过是用平方根(sqrt()
)计算斜边的方程。

你可以使用distance()
, length()
或 sqrt()
到计算屏幕的中心的距离。下面的代码包含着三个函数,毫无悬念的他们返回相同的结果。
- 注释和取消某行的注释来试试看用不同方式得到相同的结果。
上回我们把到中心的距离映射为颜色亮度。离中心越近的越暗。注意到映射值不宜过高,因为从中心(vec2(0.5, 0.5))到最远距离才刚刚超过0.5一点。仔细考察这个映射:
你能从中推断出什么?
我们怎么用这个方法来画圆?
试试有没有其他方法来实现这样画布内圆形渐变的效果。
距离场
我们可也可以从另外的角度思考上面的例子:把它当做海拔地图(等高线图)——越黑的地方意味着海拔越高。想象下,你就在圆锥的顶端,那么这里的渐变就和圆锥的等高线图有些相似。到圆锥的水平距离是一个常数0.5。这个距离值在每个方向上都是相等的。通过选择从那里截取这个圆锥,你就会得到或大或小的圆纹面。

其实我们是通过“空间距离”来重新解释什么是图形。这种技巧被称之为“距离场”,从字体轮廓到3D图形被广泛应用。
来小试下牛刀:
用step()
函数把所有大于0.5的像素点变成白色,并把小于的变成黑色(0.0)。
反转前景色和背景色。
调戏下smoothstep()
函数,用不同的值来试着做出一个边界顺滑的圆。
一旦遇到令你满意的应用,把他写成一个函数,这样将来就可以调用了。
给这个圆来些缤纷的颜色吧!
再加点动画?一闪一闪亮晶晶?或者是砰砰跳动的心脏?(或许你可以从上一章汲取一些灵感)
让它动起来?能不能移动它并且在同一个屏幕上放置多个圆?
如果你结合函数来混合不同的距离场,会发生什么呢?
1
2
3
4
5
pct = distance(st,vec2(0.4)) + distance(st,vec2(0.6));
pct = distance(st,vec2(0.4)) * distance(st,vec2(0.6));
pct = min(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = max(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = pow(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
- 用这种技巧制作三个元素,如果它们是运动的,那就再好不过啦!
添加自己的工具箱
就计算效率而言,sqrt()
函数,以及所有依赖它的运算,都耗时耗力。dot()
点乘是另外一种用来高效计算圆形距离场的方式。
距离场的特点

距离场几乎可以用来画任何东西。显然,图形越复杂,方程也越复杂。但是一旦你找到某个特定图形的公式,就很容易添加图形或应用像过渡边界的效果。正因如此,距离场经常用于字体渲染,例如Mapbox GL Labels, Matt DesLauriers Material Design Fonts 和 as is describe on Chapter 7 of iPhone 3D Programming, O’Reilly.
看看下面的代码:
我们一开始把坐标系移到中心并把它映射到-1到1之间。在 24行 这儿,我们用一个fract()
函数来呈现这个距离场产生的图案。这个距离场不断重复,就像在禅花园看到的环一样。
现在我们来看下 19行 的距离场方程。这里我们在计算点 (.3,.3)
或 vec3(.3)
到所有四象限的距离(这就是 abs()
在起作用)。
如果你取消第 20行 的注释,你会发现我们把到四个点的距离用min()
函数合并到0,并产生了一个有趣的图案。
现在再试着取消第 21行 的注释,我们做的和之前一样,只不过这次用的是 max()
函数。这次的记过是圆角矩形。注意距离场的环形是如何离中心越远越光滑的。
最后从27 行到 29 行一行行地取消注释,思考距离场的不同用途。
极坐标下的图形

在关于颜色的章节我们通过如下的方程把每个像素的 半径 和 角度 笛卡尔坐标映射到极坐标。
1
2
3
vec2 pos = vec2(0.5)-st;
float r = length(pos)*2.0;
float a = atan(pos.y,pos.x);
我们用了部分方程在这章的开头来画圆,即用 length()
计算到中心的距离。现在我们可以用极坐标来画圆。
极坐标这种方式虽然有所限制但却十分简单。
下面你会看到在同样在笛卡尔坐标下图形在极坐标下的着色器案例(在 lines 21 和 25之间)。对这些函数一个个取消注释,看看两坐标系之间的联系。
着色器之书03 - AIGISSS 着色器之书03
本文最后更新于:1 个月前
Uniforms
现在我们知道了 GPU 如何处理并行线程,每个线程负责给完整图像的一部分配置颜色。尽管每个线程和其他线程之间不能有数据交换,但我们能从 CPU 给每个线程输入数据。因为显卡的架构,所有线程的输入值必须统一(uniform),而且必须设为只读。也就是说,每条线程接收相同的数据,并且是不可改变的数据。
这些输入值叫做 uniform
(统一值),它们的数据类型通常为:float
, vec2
, vec3
, vec4
, mat2
, mat3
, mat4
, sampler2D
and samplerCube
。uniform 值需要数值类型前后一致。且在 shader 的开头,在设定精度之后,就对其进行定义。
1
2
3
4
5
6
7
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution; // 画布尺寸(宽,高)
uniform vec2 u_mouse; // 鼠标位置(在屏幕上哪个像素)
uniform float u_time; // 时间(加载后的秒数)
你可以把 uniforms 想象成连通 GPU 和 CPU 的许多小的桥梁。虽然这些 uniforms 的名字千奇百怪,但是在这一系列的例子中我一直有用到:u_time
(时间), u_resolution
(画布尺寸)和 u_mouse
(鼠标位置)。按业界传统应在 uniform 值的名字前加 u_
,这样一看即知是 uniform。尽管如此你也还会见到各种各样的名字。比如ShaderToy.com就用了如下的名字:
1
2
3
uniform vec3 iResolution; // 视口分辨率(以像素计)
uniform vec4 iMouse; // 鼠标坐标 xy: 当前位置, zw: 点击位置
uniform float iTime; // shader 运行时间(以秒计)
好了说的足够多了,我们来看看实际操作中的 uniform 吧。在下面的代码中我们使用 u_time
加上一个 sin 函数,来展示图中红色的动态变化。
GLSL 还有更多惊喜。GPU 的硬件加速支持我们使用角度,三角函数和指数函数。这里有一些这些函数的介绍:sin()
, cos()
, tan()
, asin()
, acos()
, atan()
, pow()
, exp()
, log()
, sqrt()
, abs()
, sign()
, floor()
, ceil()
, fract()
, mod()
, min()
, max()
和 clamp()
。
现在又到你来玩的时候了。
降低颜色变化的速率,直到肉眼都看不出来。
加速变化,直到颜色静止不动。
玩一玩 RGB 三个通道,分别给三个颜色不同的变化速度,看看能不能做出有趣的效果。
gl_FragCoord
就像 GLSL 有个默认输出值 vec4 gl_FragColor
一样,它也有一个默认输入值( vec4 gl_FragCoord
)。gl_FragCoord
存储了活动线程正在处理的像素或屏幕碎片的坐标。有了它我们就知道了屏幕上的哪一个线程正在运转。为什么我们不叫 gl_FragCoord
uniform (统一值)呢?因为每个像素的坐标都不同,所以我们把它叫做 varying(变化值)。
上述代码中我们用 gl_FragCoord.xy
除以 u_resolution
,对坐标进行了规范化。这样做是为了使所有的值落在 0.0
到 1.0
之间,这样就可以轻松把 X 或 Y 的值映射到红色或者绿色通道。
在 shader 的领域我们没有太多要 debug 的,更多地是试着给变量赋一些很炫的颜色,试图做出一些效果。有时你会觉得用 GLSL 编程就像是把一搜船放到了瓶子里。它同等地困难、美丽而令人满足。

现在我们来检验一下我们对上面代码的理解程度。
你明白 (0.0,0.0)
坐标在画布上的哪里吗?
那 (1.0,0.0)
, (0.0,1.0)
, (0.5,0.5)
和 (1.0,1.0)
呢?
你知道如何用未规范化(normalized)的 u_mouse
吗?你可以用它来移动颜色吗?
你可以用 u_time
和 u_mouse
来改变颜色的图案吗?不妨琢磨一些有趣的途径。
经过这些小练习后,你可能会好奇还能用强大的 shader 做什么。接下来的章节你会知道如何把你的 shader 和 three.js,Processing,和 openFrameworks 结合起来。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书03 - AIGISSS 着色器之书03
本文最后更新于:1 个月前
Uniforms
现在我们知道了 GPU 如何处理并行线程,每个线程负责给完整图像的一部分配置颜色。尽管每个线程和其他线程之间不能有数据交换,但我们能从 CPU 给每个线程输入数据。因为显卡的架构,所有线程的输入值必须统一(uniform),而且必须设为只读。也就是说,每条线程接收相同的数据,并且是不可改变的数据。
这些输入值叫做 uniform
(统一值),它们的数据类型通常为:float
, vec2
, vec3
, vec4
, mat2
, mat3
, mat4
, sampler2D
and samplerCube
。uniform 值需要数值类型前后一致。且在 shader 的开头,在设定精度之后,就对其进行定义。
1
2
3
4
5
6
7
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution; // 画布尺寸(宽,高)
uniform vec2 u_mouse; // 鼠标位置(在屏幕上哪个像素)
uniform float u_time; // 时间(加载后的秒数)
你可以把 uniforms 想象成连通 GPU 和 CPU 的许多小的桥梁。虽然这些 uniforms 的名字千奇百怪,但是在这一系列的例子中我一直有用到:u_time
(时间), u_resolution
(画布尺寸)和 u_mouse
(鼠标位置)。按业界传统应在 uniform 值的名字前加 u_
,这样一看即知是 uniform。尽管如此你也还会见到各种各样的名字。比如ShaderToy.com就用了如下的名字:
1
2
3
uniform vec3 iResolution; // 视口分辨率(以像素计)
uniform vec4 iMouse; // 鼠标坐标 xy: 当前位置, zw: 点击位置
uniform float iTime; // shader 运行时间(以秒计)
好了说的足够多了,我们来看看实际操作中的 uniform 吧。在下面的代码中我们使用 u_time
加上一个 sin 函数,来展示图中红色的动态变化。
GLSL 还有更多惊喜。GPU 的硬件加速支持我们使用角度,三角函数和指数函数。这里有一些这些函数的介绍:sin()
, cos()
, tan()
, asin()
, acos()
, atan()
, pow()
, exp()
, log()
, sqrt()
, abs()
, sign()
, floor()
, ceil()
, fract()
, mod()
, min()
, max()
和 clamp()
。
现在又到你来玩的时候了。
降低颜色变化的速率,直到肉眼都看不出来。
加速变化,直到颜色静止不动。
玩一玩 RGB 三个通道,分别给三个颜色不同的变化速度,看看能不能做出有趣的效果。
gl_FragCoord
就像 GLSL 有个默认输出值 vec4 gl_FragColor
一样,它也有一个默认输入值( vec4 gl_FragCoord
)。gl_FragCoord
存储了活动线程正在处理的像素或屏幕碎片的坐标。有了它我们就知道了屏幕上的哪一个线程正在运转。为什么我们不叫 gl_FragCoord
uniform (统一值)呢?因为每个像素的坐标都不同,所以我们把它叫做 varying(变化值)。
上述代码中我们用 gl_FragCoord.xy
除以 u_resolution
,对坐标进行了规范化。这样做是为了使所有的值落在 0.0
到 1.0
之间,这样就可以轻松把 X 或 Y 的值映射到红色或者绿色通道。
在 shader 的领域我们没有太多要 debug 的,更多地是试着给变量赋一些很炫的颜色,试图做出一些效果。有时你会觉得用 GLSL 编程就像是把一搜船放到了瓶子里。它同等地困难、美丽而令人满足。

现在我们来检验一下我们对上面代码的理解程度。
你明白 (0.0,0.0)
坐标在画布上的哪里吗?
那 (1.0,0.0)
, (0.0,1.0)
, (0.5,0.5)
和 (1.0,1.0)
呢?
你知道如何用未规范化(normalized)的 u_mouse
吗?你可以用它来移动颜色吗?
你可以用 u_time
和 u_mouse
来改变颜色的图案吗?不妨琢磨一些有趣的途径。
经过这些小练习后,你可能会好奇还能用强大的 shader 做什么。接下来的章节你会知道如何把你的 shader 和 three.js,Processing,和 openFrameworks 结合起来。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/b189f81a.html b/posts/b189f81a.html
index 0c71f5b2..eaf65936 100644
--- a/posts/b189f81a.html
+++ b/posts/b189f81a.html
@@ -1 +1 @@
-重构2——改善既有代码的设计 - AIGISSS 重构2——改善既有代码的设计
本文最后更新于:1 年前
前言
都能写出计算机可以理解代码,唯有写出人类容易理解的代码的,才是优秀的程序员。好代码的标准就是人们能否轻而易举地修改它
重构的原则
何谓重构
重构这个词既可以是名词也可以是动词,名词形式的定义的是:
对软件内部结构的调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。动词形式的定义的是:使用一系列的重构手法,在不改变可观察行为的前提下,调整其结构为何重构
- 重构改进软件的设计
- 重构时软件更容易理解
- 重构帮助找到bug
- 重构提高编程速度
何时重构
- 预备性重构:让添加的新功能更容易
- 帮助理解的重构:使代码更容易理解
- 捡垃圾式重构
- 有计划的重构和见机行事的重构
- 长期重构
- 复审代码时重构
何时不重构
- 重写比重构还容易
只有需要理解其工作原理时,对其重构才有价值。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+重构2——改善既有代码的设计 - AIGISSS 重构2——改善既有代码的设计
本文最后更新于:1 年前
前言
都能写出计算机可以理解代码,唯有写出人类容易理解的代码的,才是优秀的程序员。好代码的标准就是人们能否轻而易举地修改它
重构的原则
何谓重构
重构这个词既可以是名词也可以是动词,名词形式的定义的是:
对软件内部结构的调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。动词形式的定义的是:使用一系列的重构手法,在不改变可观察行为的前提下,调整其结构为何重构
- 重构改进软件的设计
- 重构时软件更容易理解
- 重构帮助找到bug
- 重构提高编程速度
何时重构
- 预备性重构:让添加的新功能更容易
- 帮助理解的重构:使代码更容易理解
- 捡垃圾式重构
- 有计划的重构和见机行事的重构
- 长期重构
- 复审代码时重构
何时不重构
- 重写比重构还容易
只有需要理解其工作原理时,对其重构才有价值。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/b431f548.html b/posts/b431f548.html
index 0e9c2072..9e046835 100644
--- a/posts/b431f548.html
+++ b/posts/b431f548.html
@@ -1 +1 @@
-使用coding来作为图床的方法 - AIGISSS 使用coding来作为图床的方法
本文最后更新于:5 个月前
Hello coding

本来是想同步hexo与csdn一起写。
奈何csdn不支持外链。
全部改成<img src="" />
的就可以
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+使用coding来作为图床的方法 - AIGISSS 使用coding来作为图床的方法
本文最后更新于:5 个月前
Hello coding

本来是想同步hexo与csdn一起写。
奈何csdn不支持外链。
全部改成<img src="" />
的就可以
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/b5288859.html b/posts/b5288859.html
index 30223916..12ad65ea 100644
--- a/posts/b5288859.html
+++ b/posts/b5288859.html
@@ -1,4 +1,4 @@
-着色器之书13 - AIGISSS 着色器之书13
本文最后更新于:1 个月前

分形布朗运动(Fractal Brownian Motion)
噪声对不同的人来说有不同的意义。音乐家把它当成一种令人不安的声响,通信工程师把它当作干扰信号,天体物理学家把它看作宇宙微波背景辐射。这些概念吸引着我们去探索处处可见的随机性的物理原因。但是,让我们从更基础,也更简单的开始:波和波的属性。波就是某些属性随着时间波动变化。声波是气压的波动,电磁波是电场和磁场的波动。波的两个重要特征是振幅(amplitude)和频率(frequency)。一个简单的线性波(一维)的方程如下:
着色器之书13 - AIGISSS 着色器之书13
本文最后更新于:1 个月前

分形布朗运动(Fractal Brownian Motion)
噪声对不同的人来说有不同的意义。音乐家把它当成一种令人不安的声响,通信工程师把它当作干扰信号,天体物理学家把它看作宇宙微波背景辐射。这些概念吸引着我们去探索处处可见的随机性的物理原因。但是,让我们从更基础,也更简单的开始:波和波的属性。波就是某些属性随着时间波动变化。声波是气压的波动,电磁波是电场和磁场的波动。波的两个重要特征是振幅(amplitude)和频率(frequency)。一个简单的线性波(一维)的方程如下:
着色器之书12 - AIGISSS 着色器之书12
本文最后更新于:1 个月前

网格噪声(Cellular Noise)
1996 年,在原始的 Perlin Noise 发布六年后,Perlin 的 Simplex Noise 发布五年前,Steven Worley 写了一篇名为“A Cellular Texture Basis Function”的论文。在这篇论文里,他描述了一种现在被广泛使用的程序化纹理技术。
要理解它背后的原理,我们需要从迭代开始思考。你可能已经知道迭代是什么意思:对,就是使用 for
循环。GLSL 的 for
循环中,只有一个需要注意的:我们检查循环是否继续的次数必须是一个常数(const
). 所以,没有动态循环——迭代的次数必须是固定的。
网格噪声基于距离场,这里的距离是指到一个特征点集最近的点的距离。比如说我们要写一个 4 个特征点的距离场,我们应该做什么呢?对每一个像素,计算它到最近的特征点的距离。也就是说,我们需要遍历所有 4 个特征点,计算他们到当前像素点的距离,并把最近的那个距离存下来。
1
2
3
4
5
6
float min_dist = 100.; // A variable to store the closest distance to a point
min_dist = min(min_dist, distance(st, point_a));
min_dist = min(min_dist, distance(st, point_b));
min_dist = min(min_dist, distance(st, point_c));
min_dist = min(min_dist, distance(st, point_d));

这种做法不是很优雅,但至少行得通。现在让我们用数组和 for
循环重写。
1
2
3
4
5
float m_dist = 100.; // minimum distance
for (int i = 0; i < TOTAL_POINTS; i++) {
float dist = distance(st, points[i]);
m_dist = min(m_dist, dist);
}
注意看我们用一个 for
循环遍历特征点集的数组,用一个 min()
函数来获得最小距离。下面是以上想法的简要的实现:
上面的代码中,其中一个特征点分配给了鼠标位置。把鼠标放上去玩一玩,你可以更直观地了解上面的代码是如何运行的。然后试试:
- 你可以让其余的几个特征点也动起来吗?
- 在读完关于形状的章节后,想象一些关于距离场的有意思的用法。
- 如果你要往距离场里添加更多的特征点怎么办?如果我们想动态地添加减少特征点数怎么办?
平铺和迭代
你可能注意到 GLSL 对 for
循环和 数组 似乎不太友好。如前所说,循环不接受动态的迭代次数。还有,遍历很多实例会显著地降低着色器的性能。这意味着我们不能把这个方法用在很大的特征点集上。我们需要寻找另一个策略,一个能利用 GPU 并行架构优势的策略。

解决这个问题的一个方法是把空间分割成网格。并不需要计算每一个像素点到每一个特征点的距离,对吧?已经知道每个像素点是在自己的线程中运行,我们可以把空间分割成网格(cells),每个网格对应一个特征点。另外,为避免网格交界区域的偏差,我们需要计算像素点到相邻网格中的特征点的距离。这就是 Steven Worley 的论文中的主要思想。最后,每个像素点只需要计算到九个特征点的距离:他所在的网格的特征点和相邻的八个网格的特征点。我们已经在图案,随机和噪声这些章节介绍了如何把空间分割成网格,希望你已经熟悉这项技术。
1
2
3
4
5
6
// Scale
st *= 3.;
// Tile the space
vec2 i_st = floor(st);
vec2 f_st = fract(st);
那么,计划是什么呢?我们将使用网格坐标(存储在整数坐标 i_st
中)来构造特征点的随机位置。random2f
函数接受一个 vec2
类型参数,返回给我们一个 vec2
类型的随机位置。所以,在每个网格内,我们有一个特征点在随机位置上。
1
vec2 point = random2(i_st);
网格内的每个像素点(存储在浮点坐标 f_st
中)都会计算它到那个随机点的距离。
1
2
vec2 diff = point - f_st;
float dist = length(diff);
结果看起来就像这样:
我们还需要计算像素点到相邻网格中随机点的距离,而不只是当前的网格。我们需要 遍历 所有相邻网格。不是所有网格,仅仅是那些和当前网格相邻的网格。从网格坐标来说,就是 x
坐标从 -1
(左)到 1
(右), y
坐标从 -1
(下)到 1
(上)。一个 9 个网格的 3x3 区域可以用两个 for
循环遍历:
1
2
3
4
5
6
7
for (int y= -1; y <= 1; y++) {
for (int x= -1; x <= 1; x++) {
// Neighbor place in the grid
vec2 neighbor = vec2(float(x),float(y));
...
}
}

现在,我们可以在双 for
循环中计算相邻网格中随机点的位置,只需要加上相邻网格对当前网格的偏移量。
1
2
3
4
...
// Random position from current + neighbor place in the grid
vec2 point = random2(i_st + neighbor);
...
剩下的部分就是计算像素点到那个随机点的距离,并把最近的距离存到变量 m_dist
(minimum distance)里面.
1
2
3
4
5
6
7
8
9
...
vec2 diff = neighbor + point - f_st;
// Distance to the point
float dist = length(diff);
// Keep the closer distance
m_dist = min(m_dist, dist);
...
上面的代码源自这篇 Inigo’s Quilez 的文章,他写道:
“可能值得注意的是,上面的代码中有一个很漂亮的技巧。多数实现都存在精度问题,因为他们是在“域”空间(如“世界”或“对象”空间)内产生随机点,这可能是里原点任意远的。要解决这个问题,可以使用更高精度的数据类型,或更聪明些。我的实现不是在“域”空间(如“世界”或“对象”空间)内产生随机点,而是在“网格”空间内:一旦提取了着色点的整数和小数部分,我们当前的网格就确定了,我们所关心的就是这个网格周围发生了什么,意味着我们可以将所有坐标的整数部分放在一起,从而节省了许多精度位。事实上,一个常规的 voronoi 实现中,从着色点减去随机特征点时,点坐标的整数部分简单地消除掉了。上面的实现中,我们甚至不会让这种消除发生,因为我们正在把所有的计算移到“网格”空间。这个技巧可以让你处理这种情况: 你想要把 voronoi 用在整个星球上——可以简单地将输入替换为双精度,执行 floor() 和 fract() 计算,其余的计算仍使用浮点数,省去了将整个实现改成双精度的成本。当然,同样的技巧也适用于 Perlin Noise 模式(但是我还没有在任何地方看到过它的实现或记录)。”
简要重述一遍:我们把空间分割成网格,计算每个像素点到它所在网格中的那个特征点的距离,和它到相邻的八个网格中的特征点的距离,结果是一个距离场,如下所示:
进一步探索:
- 缩放空间。
- 你有其它办法让那些特征点动起来吗?
- 如果我们想要加入一个鼠标位置作为其中一个特征点怎么办?
- 有没有其它办法构造这个距离场,除了
m_dist = min(m_dist, dist);
之外? - 用这个距离场你可以创造出什么有意思的图案?
这个算法也可以从特征点而非像素点的角度理解。在那种情况下,算法可以表述为:每个特征点向外扩张生长,直到它碰到其它扩张的区域。这反映了自然界的生长规则。生命的形态是由内部扩张、生长的力量和限制性的外部力量共同决定的。模拟这种行为的算法以 Georgy Voronoi 命名。

Voronoi 算法
用网格噪声构造 Voronoi 图远没有看上去的那么难。我们只需要保留一些关于最近的特征点的额外信息。我们将要用到一个叫 m_point
的 vec2
类型变量存储像素点到最近的特征点的向量,而不只是距离。
1
2
3
4
5
6
...
if( dist < m_dist ) {
m_dist = dist;
m_point = point;
}
...
注意在下面的代码中,我们不再使用 min
来计算最近距离,而是用一个普通的 if
语句。为什么?因为当一个新的更近的特征点出现的时候,我们还需要保存它的位置(32 行至 37行)。
注意那个移动的(鼠标位置下面那个)细胞的颜色是如何根据它的位置而改变的。那是因为它的颜色由最近特征点决定。
就像我们之前所做的那样,现在是扩大规模的时候,转而使用 Steven Worley 的论文中的方法。试着自己实现它。你可以通过点击下面的示例来获取帮助。注意 Steven Worley 的原始方法中,每个网格的特征点数是可变的,对大多数网格来说不止一个。在他的 C 语言实现中,这是用来提早退出来加速循环。GLSL 循环不允许动态的迭代次数,所以你可能更希望一个网格对应一个特征点。
一旦你弄清楚了这个算法,想想它有什么有趣、有创意的用途。

优化 Voronoi
在 2011 年, Stefan Gustavson 优化了 Steven Worley 的算法,仅仅对一个 2x2 的矩阵作遍历(而不是 3x3 的矩阵)。这显著地减少了工作量,但是会在网格边缘制造人工痕迹。看看下面的例子。
Later in 2012 Inigo Quilez wrote an article on how to make precise Voronoi borders.
Inigo 在 Voronoi 上的实验并没有就此停止。2014 年,他写了一篇非常漂亮的文章,提出一种他称作为 voro-noise 的噪声,可以让常规噪声和 voronoi 逐渐地融合。用他的话说:
“尽管有这样的相似之处,但事实上,两种模式中网格的使用方式都是不同的。噪声会内插或平均随机值(如值噪声),而 Voronoi 是计算到最近特征点的距离。平滑双线性插值和最小值评估是两个非常不同的操作,或者……它们是否能用更广义的方法组合?如果是这样,那么噪声和 Voronoi 模式都可以被看作是一种更一般的以网格为基础的模式生成器?”
现在,是时候仔细观察事物,去接受自然的启发,并用这项技术发现你自己的风景!

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书12 - AIGISSS 着色器之书12
本文最后更新于:1 个月前

网格噪声(Cellular Noise)
1996 年,在原始的 Perlin Noise 发布六年后,Perlin 的 Simplex Noise 发布五年前,Steven Worley 写了一篇名为“A Cellular Texture Basis Function”的论文。在这篇论文里,他描述了一种现在被广泛使用的程序化纹理技术。
要理解它背后的原理,我们需要从迭代开始思考。你可能已经知道迭代是什么意思:对,就是使用 for
循环。GLSL 的 for
循环中,只有一个需要注意的:我们检查循环是否继续的次数必须是一个常数(const
). 所以,没有动态循环——迭代的次数必须是固定的。
网格噪声基于距离场,这里的距离是指到一个特征点集最近的点的距离。比如说我们要写一个 4 个特征点的距离场,我们应该做什么呢?对每一个像素,计算它到最近的特征点的距离。也就是说,我们需要遍历所有 4 个特征点,计算他们到当前像素点的距离,并把最近的那个距离存下来。
1
2
3
4
5
6
float min_dist = 100.; // A variable to store the closest distance to a point
min_dist = min(min_dist, distance(st, point_a));
min_dist = min(min_dist, distance(st, point_b));
min_dist = min(min_dist, distance(st, point_c));
min_dist = min(min_dist, distance(st, point_d));

这种做法不是很优雅,但至少行得通。现在让我们用数组和 for
循环重写。
1
2
3
4
5
float m_dist = 100.; // minimum distance
for (int i = 0; i < TOTAL_POINTS; i++) {
float dist = distance(st, points[i]);
m_dist = min(m_dist, dist);
}
注意看我们用一个 for
循环遍历特征点集的数组,用一个 min()
函数来获得最小距离。下面是以上想法的简要的实现:
上面的代码中,其中一个特征点分配给了鼠标位置。把鼠标放上去玩一玩,你可以更直观地了解上面的代码是如何运行的。然后试试:
- 你可以让其余的几个特征点也动起来吗?
- 在读完关于形状的章节后,想象一些关于距离场的有意思的用法。
- 如果你要往距离场里添加更多的特征点怎么办?如果我们想动态地添加减少特征点数怎么办?
平铺和迭代
你可能注意到 GLSL 对 for
循环和 数组 似乎不太友好。如前所说,循环不接受动态的迭代次数。还有,遍历很多实例会显著地降低着色器的性能。这意味着我们不能把这个方法用在很大的特征点集上。我们需要寻找另一个策略,一个能利用 GPU 并行架构优势的策略。

解决这个问题的一个方法是把空间分割成网格。并不需要计算每一个像素点到每一个特征点的距离,对吧?已经知道每个像素点是在自己的线程中运行,我们可以把空间分割成网格(cells),每个网格对应一个特征点。另外,为避免网格交界区域的偏差,我们需要计算像素点到相邻网格中的特征点的距离。这就是 Steven Worley 的论文中的主要思想。最后,每个像素点只需要计算到九个特征点的距离:他所在的网格的特征点和相邻的八个网格的特征点。我们已经在图案,随机和噪声这些章节介绍了如何把空间分割成网格,希望你已经熟悉这项技术。
1
2
3
4
5
6
// Scale
st *= 3.;
// Tile the space
vec2 i_st = floor(st);
vec2 f_st = fract(st);
那么,计划是什么呢?我们将使用网格坐标(存储在整数坐标 i_st
中)来构造特征点的随机位置。random2f
函数接受一个 vec2
类型参数,返回给我们一个 vec2
类型的随机位置。所以,在每个网格内,我们有一个特征点在随机位置上。
1
vec2 point = random2(i_st);
网格内的每个像素点(存储在浮点坐标 f_st
中)都会计算它到那个随机点的距离。
1
2
vec2 diff = point - f_st;
float dist = length(diff);
结果看起来就像这样:
我们还需要计算像素点到相邻网格中随机点的距离,而不只是当前的网格。我们需要 遍历 所有相邻网格。不是所有网格,仅仅是那些和当前网格相邻的网格。从网格坐标来说,就是 x
坐标从 -1
(左)到 1
(右), y
坐标从 -1
(下)到 1
(上)。一个 9 个网格的 3x3 区域可以用两个 for
循环遍历:
1
2
3
4
5
6
7
for (int y= -1; y <= 1; y++) {
for (int x= -1; x <= 1; x++) {
// Neighbor place in the grid
vec2 neighbor = vec2(float(x),float(y));
...
}
}

现在,我们可以在双 for
循环中计算相邻网格中随机点的位置,只需要加上相邻网格对当前网格的偏移量。
1
2
3
4
...
// Random position from current + neighbor place in the grid
vec2 point = random2(i_st + neighbor);
...
剩下的部分就是计算像素点到那个随机点的距离,并把最近的距离存到变量 m_dist
(minimum distance)里面.
1
2
3
4
5
6
7
8
9
...
vec2 diff = neighbor + point - f_st;
// Distance to the point
float dist = length(diff);
// Keep the closer distance
m_dist = min(m_dist, dist);
...
上面的代码源自这篇 Inigo’s Quilez 的文章,他写道:
“可能值得注意的是,上面的代码中有一个很漂亮的技巧。多数实现都存在精度问题,因为他们是在“域”空间(如“世界”或“对象”空间)内产生随机点,这可能是里原点任意远的。要解决这个问题,可以使用更高精度的数据类型,或更聪明些。我的实现不是在“域”空间(如“世界”或“对象”空间)内产生随机点,而是在“网格”空间内:一旦提取了着色点的整数和小数部分,我们当前的网格就确定了,我们所关心的就是这个网格周围发生了什么,意味着我们可以将所有坐标的整数部分放在一起,从而节省了许多精度位。事实上,一个常规的 voronoi 实现中,从着色点减去随机特征点时,点坐标的整数部分简单地消除掉了。上面的实现中,我们甚至不会让这种消除发生,因为我们正在把所有的计算移到“网格”空间。这个技巧可以让你处理这种情况: 你想要把 voronoi 用在整个星球上——可以简单地将输入替换为双精度,执行 floor() 和 fract() 计算,其余的计算仍使用浮点数,省去了将整个实现改成双精度的成本。当然,同样的技巧也适用于 Perlin Noise 模式(但是我还没有在任何地方看到过它的实现或记录)。”
简要重述一遍:我们把空间分割成网格,计算每个像素点到它所在网格中的那个特征点的距离,和它到相邻的八个网格中的特征点的距离,结果是一个距离场,如下所示:
进一步探索:
- 缩放空间。
- 你有其它办法让那些特征点动起来吗?
- 如果我们想要加入一个鼠标位置作为其中一个特征点怎么办?
- 有没有其它办法构造这个距离场,除了
m_dist = min(m_dist, dist);
之外? - 用这个距离场你可以创造出什么有意思的图案?
这个算法也可以从特征点而非像素点的角度理解。在那种情况下,算法可以表述为:每个特征点向外扩张生长,直到它碰到其它扩张的区域。这反映了自然界的生长规则。生命的形态是由内部扩张、生长的力量和限制性的外部力量共同决定的。模拟这种行为的算法以 Georgy Voronoi 命名。

Voronoi 算法
用网格噪声构造 Voronoi 图远没有看上去的那么难。我们只需要保留一些关于最近的特征点的额外信息。我们将要用到一个叫 m_point
的 vec2
类型变量存储像素点到最近的特征点的向量,而不只是距离。
1
2
3
4
5
6
...
if( dist < m_dist ) {
m_dist = dist;
m_point = point;
}
...
注意在下面的代码中,我们不再使用 min
来计算最近距离,而是用一个普通的 if
语句。为什么?因为当一个新的更近的特征点出现的时候,我们还需要保存它的位置(32 行至 37行)。
注意那个移动的(鼠标位置下面那个)细胞的颜色是如何根据它的位置而改变的。那是因为它的颜色由最近特征点决定。
就像我们之前所做的那样,现在是扩大规模的时候,转而使用 Steven Worley 的论文中的方法。试着自己实现它。你可以通过点击下面的示例来获取帮助。注意 Steven Worley 的原始方法中,每个网格的特征点数是可变的,对大多数网格来说不止一个。在他的 C 语言实现中,这是用来提早退出来加速循环。GLSL 循环不允许动态的迭代次数,所以你可能更希望一个网格对应一个特征点。
一旦你弄清楚了这个算法,想想它有什么有趣、有创意的用途。

优化 Voronoi
在 2011 年, Stefan Gustavson 优化了 Steven Worley 的算法,仅仅对一个 2x2 的矩阵作遍历(而不是 3x3 的矩阵)。这显著地减少了工作量,但是会在网格边缘制造人工痕迹。看看下面的例子。
Later in 2012 Inigo Quilez wrote an article on how to make precise Voronoi borders.
Inigo 在 Voronoi 上的实验并没有就此停止。2014 年,他写了一篇非常漂亮的文章,提出一种他称作为 voro-noise 的噪声,可以让常规噪声和 voronoi 逐渐地融合。用他的话说:
“尽管有这样的相似之处,但事实上,两种模式中网格的使用方式都是不同的。噪声会内插或平均随机值(如值噪声),而 Voronoi 是计算到最近特征点的距离。平滑双线性插值和最小值评估是两个非常不同的操作,或者……它们是否能用更广义的方法组合?如果是这样,那么噪声和 Voronoi 模式都可以被看作是一种更一般的以网格为基础的模式生成器?”
现在,是时候仔细观察事物,去接受自然的启发,并用这项技术发现你自己的风景!

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/c5427cd6.html b/posts/c5427cd6.html
index d45d94f4..525bec8d 100644
--- a/posts/c5427cd6.html
+++ b/posts/c5427cd6.html
@@ -1 +1 @@
-着色器之书16 - AIGISSS 着色器之书16
本文最后更新于:1 个月前
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书16 - AIGISSS 着色器之书16
本文最后更新于:1 个月前
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/cb3db6e0.html b/posts/cb3db6e0.html
index da09ffb8..3feb7a66 100644
--- a/posts/cb3db6e0.html
+++ b/posts/cb3db6e0.html
@@ -1 +1 @@
-mapboxgl导入模型时遇到的问题总结 - AIGISSS mapboxgl导入模型时遇到的问题总结
本文最后更新于:7 个月前
前言:使用mapboxgl导入模型时,经常导不进去场景里面 会出很多差错,这里给大家分享一些心得。此文来源我同事小阮!
模型整合
我们知道一个稍微一点复杂的模型,都是由很多很多的几何体组成的,不过你不把所有模型整合到一起可能导致在mapbox中无法加载的哟!
具体做法,如图一:
随便选则一个小小的建筑合集(或者任何一个小的几何体)右键该模型将他转化为 可编辑多边形(或者可编辑网格)
选中该可编辑多边形 找到右边的属性栏 找到附加 功能,如图二:
点击附加,附加页面里面按 ctrl+a 全选,如图三:
这样他就变成了一个整体模型哟,如图四:
ps:如果下载的文件已经一个整体就不用这个操作了!!!
材质整合
不只是模型要整合哦,材质也要整合哦,不过这个过程实在是太复杂。总而言之就是 要把所有材质集合成一个多维子材质!具体教程可以百度经验里面有https://jingyan.baidu.com/arti….cle/afd8f4dedbd51234
如果你的材质不是多维子材质 可能会导致 无法导致场景里面哟
ps:如果下载模型的材质已经是多维材质的话也不用这个操作啦!!
模型导出
般的话导出 obj 和 mtl 文件都会有一个选项建议参数这样选择哟,如图五。
导出完了之后 会有一个map文件夹,里面有你有的一些材质,这里注意一下所有的材质文件的名称必须要用中文!!!!!但是有的时候3dsmax会给你自己自动取名为中文,当你把中文去掉之后 你还要打开mtl文件(用vscode打开就可以)修改一下mtl里面的材质路径。说到这里 ,通过查看mtl文件还可以查看你的材质放在哪个目录这个也很关键哦!通常网上的一些作者材质都是乱放的,你可以通过修改mtl文件里面的路径 来找到你的材质文件。
坐标系偏移问题
这个问题是今天碰到一个新问题,就是博阳在mapbox里面根本找不到文件在哪里,在cesium加载的模型实际坐标相差十万八千里。如图七:
当时我没有反应过来,知道博阳提到了我自己模型的坐标的问题,我才想起来,这个问题处理起来很简单很简单,只要几秒钟。选中模型按下W键 让模型变成可移动状态,然后按f11 把绝对:世界 下面的 xyz轴的参数全部调成0,这样模型就乖乖回到原点啦。
除此之外我想提到一个轴的概念,任何一个模型/物体 都有自己的轴,我们进行平移,旋转,缩放功能都是基于这个轴来进行的,默认情况下 这个轴是在 物体的中心的 但是今天下载的这个模型的轴呢偏离的特别夸张,如果你之后还想对这个模型进行缩放,旋转,平移操作的话建议把这个轴移到这个物体的中心,具体做法也非常简单百度一下就造了
ps 怎么判断这个物体是有偏移呢?不可能每一次都先导出了 然技术端试了偏了十万八千里再来改把,这样会给技术研发同事带来很多困扰
就像今天一样搞了一个小时才找到问题
这里有一个很简单的办法,就是每次我们打开3dsmax 主界面都会一个网格线 ,如图八:

如果你的模型在这个网格线上的话 就是标准的没有偏移很多哟
渲染器问题
当你确认了mtl文件没有问题,有模型框架缺没有材质,这个时候可能是的渲染器没有选对哦!现在市面上百分之八十3dsmax作者使用的都是vray渲染器,但是我们的mapbox场景不支持vray渲染器提供的材质和渲染效果,如果你购买的max模型使是用vray渲染器的话你要按f10调处渲染器窗口,将vray渲染器改成扫描线渲染器(默认渲染器),之后的材质颜色还是要靠你自己调整哟
备注:还有一个很简单的办法 可以看这个场景是否可以导入到mapbox场景里面哟! 就是用3d查看器 直接打开glb/gltf格式的文件,如果材质和形状没有问题,说明 材质 和模型都可以正常显示(但是不能确定他的坐标问题!!今天我就是这样以为材质啥的没问题,结果偏到西班牙去了)
以后可能多多少少还有很多问题,希望大家可以互相学习,一起努力,总结经验 少走弯路!!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+mapboxgl导入模型时遇到的问题总结 - AIGISSS mapboxgl导入模型时遇到的问题总结
本文最后更新于:1 分钟前
前言:使用mapboxgl导入模型时,经常导不进去场景里面 会出很多差错,这里给大家分享一些心得。此文来源我同事小阮!
模型整合
我们知道一个稍微一点复杂的模型,都是由很多很多的几何体组成的,不过你不把所有模型整合到一起可能导致在mapbox中无法加载的哟!
具体做法,如图一:
随便选则一个小小的建筑合集(或者任何一个小的几何体)右键该模型将他转化为 可编辑多边形(或者可编辑网格)
选中该可编辑多边形 找到右边的属性栏 找到附加 功能,如图二:
点击附加,附加页面里面按 ctrl+a 全选,如图三:
这样他就变成了一个整体模型哟,如图四:
ps:如果下载的文件已经一个整体就不用这个操作了!!!
材质整合
不只是模型要整合哦,材质也要整合哦,不过这个过程实在是太复杂。总而言之就是 要把所有材质集合成一个多维子材质!具体教程可以百度经验里面有https://jingyan.baidu.com/arti….cle/afd8f4dedbd51234
如果你的材质不是多维子材质 可能会导致 无法导致场景里面哟
ps:如果下载模型的材质已经是多维材质的话也不用这个操作啦!!
模型导出
般的话导出 obj 和 mtl 文件都会有一个选项建议参数这样选择哟,如图五。
导出完了之后 会有一个map文件夹,里面有你有的一些材质,这里注意一下所有的材质文件的名称必须要用中文!!!!!但是有的时候3dsmax会给你自己自动取名为中文,当你把中文去掉之后 你还要打开mtl文件(用vscode打开就可以)修改一下mtl里面的材质路径。说到这里 ,通过查看mtl文件还可以查看你的材质放在哪个目录这个也很关键哦!通常网上的一些作者材质都是乱放的,你可以通过修改mtl文件里面的路径 来找到你的材质文件。
坐标系偏移问题
这个问题是今天碰到一个新问题,就是博阳在mapbox里面根本找不到文件在哪里,在cesium加载的模型实际坐标相差十万八千里。如图七:
当时我没有反应过来,知道博阳提到了我自己模型的坐标的问题,我才想起来,这个问题处理起来很简单很简单,只要几秒钟。选中模型按下W键 让模型变成可移动状态,然后按f11 把绝对:世界 下面的 xyz轴的参数全部调成0,这样模型就乖乖回到原点啦。
除此之外我想提到一个轴的概念,任何一个模型/物体 都有自己的轴,我们进行平移,旋转,缩放功能都是基于这个轴来进行的,默认情况下 这个轴是在 物体的中心的 但是今天下载的这个模型的轴呢偏离的特别夸张,如果你之后还想对这个模型进行缩放,旋转,平移操作的话建议把这个轴移到这个物体的中心,具体做法也非常简单百度一下就造了
ps 怎么判断这个物体是有偏移呢?不可能每一次都先导出了 然技术端试了偏了十万八千里再来改把,这样会给技术研发同事带来很多困扰
就像今天一样搞了一个小时才找到问题
这里有一个很简单的办法,就是每次我们打开3dsmax 主界面都会一个网格线 ,如图八:

如果你的模型在这个网格线上的话 就是标准的没有偏移很多哟
渲染器问题
当你确认了mtl文件没有问题,有模型框架缺没有材质,这个时候可能是的渲染器没有选对哦!现在市面上百分之八十3dsmax作者使用的都是vray渲染器,但是我们的mapbox场景不支持vray渲染器提供的材质和渲染效果,如果你购买的max模型使是用vray渲染器的话你要按f10调处渲染器窗口,将vray渲染器改成扫描线渲染器(默认渲染器),之后的材质颜色还是要靠你自己调整哟
备注:还有一个很简单的办法 可以看这个场景是否可以导入到mapbox场景里面哟! 就是用3d查看器 直接打开glb/gltf格式的文件,如果材质和形状没有问题,说明 材质 和模型都可以正常显示(但是不能确定他的坐标问题!!今天我就是这样以为材质啥的没问题,结果偏到西班牙去了)
以后可能多多少少还有很多问题,希望大家可以互相学习,一起努力,总结经验 少走弯路!!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/cc12ec28.html b/posts/cc12ec28.html
index a6237c83..322eabf1 100644
--- a/posts/cc12ec28.html
+++ b/posts/cc12ec28.html
@@ -1 +1 @@
-在centos上搭建git服务器并自动同步代码 - AIGISSS 在centos上搭建git服务器并自动同步代码
本文最后更新于:7 个月前
本篇内容用来讲述如何将 hexo 博客部署到云服务器上。
只要通过三步即可成功部署:
- 云服务器端 git 的配置
- Nginx 的配置
- 本地端 hexo 的设置更改
云服务器端配置 git
可以直接使用 yum install git 安装,也可以使用编译安装,如下:安装依赖库和编译工具
安装依赖库:
1
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel
安装编译工具:
1
yum install gcc perl-ExtUtils-MakeMaker package
下载 git
选择一个目录来存放下载下来的 git 安装包。这里选择了/usr/local/src
目录
1
cd /usr/local/src
到官网找一个新版稳定的源码包下载到 /usr/local/src
文件夹里
1
wget https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
解压编译 git
在当前目录下解压 git-2.16.2.tar.gz
1
tar -zvxf git-2.16.2.tar.gz
进入 git-2.16.2.tar.gz
目录下
1
cd git-2.16.2
执行编译
1
make all prefix=/usr/local/git
安装 git 到 /usr/local/git
目录下
1
make install prefix=/usr/local/git
配置 git 环境变量
将 git 加入 PATH 目录中
1
echo 'export PATH=$PATH:/usr/local/git/bin' >> /etc/bashrc
使 git 环境变量生效
1
source /etc/bashrc
查看 git 版本
1
git --version
如果此时能查看到 git 的版本号,说明我们已经安装成功了。
创建 git 仓库,用于存放博客网站资源。
在 home/git
的目录下,创建一个名为hexoBlog
的裸仓库(bare repo)。
如果没有 home/git
目录,需要先创建;然后修改目录的所有权和用户权限。
1
2
3
mkdir /home/git/
chown -R $USER:$USER /home/git/
chmod -R 755 /home/git/
然后,执行如下命令:
1
2
cd /home/git/
git init --bare aigisss.git
刚才这一步主要创建一个裸的 git 仓库。
创建一个新的 git 钩子,用于自动部署。
在 /home/git/aigisss.git
下,有一个自动生成的 hooks
文件夹。我们需要在里边新建一个新的钩子文件 post-receive
。
1
vim /home/git/aigisss.git/hooks/post-receive
按 i
键进入文件的编辑模式,在该文件中添加两行代码(将下边的代码粘贴进去),指定 Git 的工作树(源代码)和 Git 目录(配置文件等)
1
2
#!/bin/bash
git --work-tree=/home/web/aigisss --git-dir=/home/git/aigisss.git checkout -f
然后,按 Esc
键退出编辑模式,输入:wq
保存退出。
修改文件权限,使得其可执行
1
chmod +x /home/git/aigisss.git/hooks/post-receive
到这里,我们的 git 仓库算是完全搭建好了。下面进行 Nginx
的配置。
云服务器端配置 Nginx
安装 Nginx
1
yum install -y nginx
启动 Nginx
1
service nginx start
测试 Nginx 服务器
1
wget https://127.0.0.1
能够正常获取欢迎页面说明Nginx安装成功。
1
2
3
4
5
6
7
8
Connecting to 127.0.0.1:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 43704 (43K) [text/html]
Saving to: ‘index.html’
100%[=======================================>] 43,704 --.-K/s in 0s
2018-03-09 23:04:09 (487 MB/s) - ‘index.html’ saved [43704/43704]
测试网页是否能打开
在浏览器中输入服务器 ip 地址,就是服务器的公网 ip。
配置 Nginx
托管文件目录
接下来,创建 /home/hexoBlog
目录,用于 Nginx
托管。
1
2
3
mkdir /home/web/aigisss/
chown -R $USER:$USER /home/web/aigisss/
chmod -R 755 /home/web/aigisss/
查看 Nginx
的默认配置的安装位置
1
nginx -t
修改Nginx
的默认配置,其中 cd
后边就是刚才查到的安装位置(每个人可能都不一样)
1
vim /etc/nginx/nginx.conf
按方向键,找到如下位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80 default_server;
listen [::]:80 default_server;
root /home/hexoBlog; #需要修改
server_name www.bujige.net; #需要修改
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
}
error_page 404 /404.html;
location = /40x.html {
}
按i
键进入插入模式,将其中的 root 值改为 /home/hexoBlog
(刚才创建的托管仓库目录)。
将 server_name 值改成你的域名。
重启 Nginx
服务
1
service nginx restart
至此,服务器端配置就结束了。接下来,就剩下本地 hexo
的配置更改了。
修改 hexo 站点配置文件 git 相关设置
打开你本地的 hexo 博客所在文件,打开站点配置文件(不是主题配置文件),做以下修改。
1
2
3
4
deploy:
type: git
repo: root@CVM 你的云服务器的IP地址:/home/git/aigisss
branch: master
在 hexo 目录下执行部署,试试看。
1
2
3
4
cd 你的 hexo 目录
hexo clean
hexo generate
hexo deploy
打开你的公网 IP,看是不是已经部署成功了。
最后一步,更改域名解析。这一步不再做介绍。
1
cert --nginx --nginx-server-root=/usr/local/nginx/conf -d www.aigisss.com
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+在centos上搭建git服务器并自动同步代码 - AIGISSS 在centos上搭建git服务器并自动同步代码
本文最后更新于:1 分钟前
本篇内容用来讲述如何将 hexo 博客部署到云服务器上。
只要通过三步即可成功部署:
- 云服务器端 git 的配置
- Nginx 的配置
- 本地端 hexo 的设置更改
云服务器端配置 git
可以直接使用 yum install git 安装,也可以使用编译安装,如下:安装依赖库和编译工具
安装依赖库:
1
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel
安装编译工具:
1
yum install gcc perl-ExtUtils-MakeMaker package
下载 git
选择一个目录来存放下载下来的 git 安装包。这里选择了/usr/local/src
目录
1
cd /usr/local/src
到官网找一个新版稳定的源码包下载到 /usr/local/src
文件夹里
1
wget https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
解压编译 git
在当前目录下解压 git-2.16.2.tar.gz
1
tar -zvxf git-2.16.2.tar.gz
进入 git-2.16.2.tar.gz
目录下
1
cd git-2.16.2
执行编译
1
make all prefix=/usr/local/git
安装 git 到 /usr/local/git
目录下
1
make install prefix=/usr/local/git
配置 git 环境变量
将 git 加入 PATH 目录中
1
echo 'export PATH=$PATH:/usr/local/git/bin' >> /etc/bashrc
使 git 环境变量生效
1
source /etc/bashrc
查看 git 版本
1
git --version
如果此时能查看到 git 的版本号,说明我们已经安装成功了。
创建 git 仓库,用于存放博客网站资源。
在 home/git
的目录下,创建一个名为hexoBlog
的裸仓库(bare repo)。
如果没有 home/git
目录,需要先创建;然后修改目录的所有权和用户权限。
1
2
3
mkdir /home/git/
chown -R $USER:$USER /home/git/
chmod -R 755 /home/git/
然后,执行如下命令:
1
2
cd /home/git/
git init --bare aigisss.git
刚才这一步主要创建一个裸的 git 仓库。
创建一个新的 git 钩子,用于自动部署。
在 /home/git/aigisss.git
下,有一个自动生成的 hooks
文件夹。我们需要在里边新建一个新的钩子文件 post-receive
。
1
vim /home/git/aigisss.git/hooks/post-receive
按 i
键进入文件的编辑模式,在该文件中添加两行代码(将下边的代码粘贴进去),指定 Git 的工作树(源代码)和 Git 目录(配置文件等)
1
2
#!/bin/bash
git --work-tree=/home/web/aigisss --git-dir=/home/git/aigisss.git checkout -f
然后,按 Esc
键退出编辑模式,输入:wq
保存退出。
修改文件权限,使得其可执行
1
chmod +x /home/git/aigisss.git/hooks/post-receive
到这里,我们的 git 仓库算是完全搭建好了。下面进行 Nginx
的配置。
云服务器端配置 Nginx
安装 Nginx
1
yum install -y nginx
启动 Nginx
1
service nginx start
测试 Nginx 服务器
1
wget https://127.0.0.1
能够正常获取欢迎页面说明Nginx安装成功。
1
2
3
4
5
6
7
8
Connecting to 127.0.0.1:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 43704 (43K) [text/html]
Saving to: ‘index.html’
100%[=======================================>] 43,704 --.-K/s in 0s
2018-03-09 23:04:09 (487 MB/s) - ‘index.html’ saved [43704/43704]
测试网页是否能打开
在浏览器中输入服务器 ip 地址,就是服务器的公网 ip。
配置 Nginx
托管文件目录
接下来,创建 /home/hexoBlog
目录,用于 Nginx
托管。
1
2
3
mkdir /home/web/aigisss/
chown -R $USER:$USER /home/web/aigisss/
chmod -R 755 /home/web/aigisss/
查看 Nginx
的默认配置的安装位置
1
nginx -t
修改Nginx
的默认配置,其中 cd
后边就是刚才查到的安装位置(每个人可能都不一样)
1
vim /etc/nginx/nginx.conf
按方向键,找到如下位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80 default_server;
listen [::]:80 default_server;
root /home/hexoBlog; #需要修改
server_name www.bujige.net; #需要修改
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
}
error_page 404 /404.html;
location = /40x.html {
}
按i
键进入插入模式,将其中的 root 值改为 /home/hexoBlog
(刚才创建的托管仓库目录)。
将 server_name 值改成你的域名。
重启 Nginx
服务
1
service nginx restart
至此,服务器端配置就结束了。接下来,就剩下本地 hexo
的配置更改了。
修改 hexo 站点配置文件 git 相关设置
打开你本地的 hexo 博客所在文件,打开站点配置文件(不是主题配置文件),做以下修改。
1
2
3
4
deploy:
type: git
repo: root@CVM 你的云服务器的IP地址:/home/git/aigisss
branch: master
在 hexo 目录下执行部署,试试看。
1
2
3
4
cd 你的 hexo 目录
hexo clean
hexo generate
hexo deploy
打开你的公网 IP,看是不是已经部署成功了。
最后一步,更改域名解析。这一步不再做介绍。
1
cert --nginx --nginx-server-root=/usr/local/nginx/conf -d www.aigisss.com
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/d5d49586.html b/posts/d5d49586.html
index 8dc8d636..3e5661f1 100644
--- a/posts/d5d49586.html
+++ b/posts/d5d49586.html
@@ -1 +1 @@
-git的操作记录 - AIGISSS git的操作记录
本文最后更新于:1 年前
与服务器上的代码产生冲突
如果系统中有一些配置文件在服务器上做了配置修改,然后后续开发又新添加一些配置项的时候,在发布这个配置文件的时候,会发生代码冲突:
error: Your local changes to the following files would be overwritten by merge:
protected/config/main.php
Please, commit your changes or stash them before you can merge.
如果希望保留生产服务器上所做的改动,仅仅并入新配置项, 处理方法如下:
1
2
3
git stash
git pull
git stash pop
然后可以使用 git diff -w +文件名
来确认代码自动合并的情况.
反过来,如果希望用代码库中的文件完全覆盖本地工作版本. 方法如下:
1
2
git reset --hard
git pull
其中 git reset
是针对版本,如果想针对文件回退本地修改,使用
1
git checkout HEAD file/to/restore
辛辛苦苦加班一星期敲的代码没了
过程是这样的,在终端输入 git log,列出所有的 commit 信息,如下图:

commit 的信息很简单,就是做了 6 个功能,每个功能对应一个 commit 的提交,分别是 feature-1 到 feature-6。
接下来执行了强制回滚,如下:
1
git reset --hard 2216d4e
回滚到了 feature-1 上,并且回滚的时候加了—hard,导致之前 feature-2 到 feature-6 的所有代码全部弄丢了,现在 git log 的显示如下:

现在 feature-2 到 feature-6 的代码没了。。。。。
然鹅还没完,在这个基础上新添加了一个 commit 提交,信息叫 feature-7,如下图:

现在 feature-2 到 feature-6 全没了,还多了一个 feature-7
请问 如何把丢失的代码 feature-2 到 feature-6 全部恢复回来,并且 feature-7 的代码也要保留
用 git reflog 和 git cherry-pick 就能解决
在终端里输入:
1
git reflog
然后就会展示出所有你之前 git 操作,你以前所有的操作都被 git 记录了下来,如下图:

这时候要记好两个值:4c97ff3 和 cd52afc,他们分别是 feature-7 和 feature-6 的 hash 码。然后执行回滚,回到 feature-6 上:
1
git reset --hard cd52afc
现在我们回到了 feature-6 上,如下图:

我们回到了 feature-6 上,但是 feature-7 没了,如何加上来呢?
这个时候就用上了 git cherry-pick,刚刚我们知道了 feature-7 的 hash 码为 4c97ff3,操作如下:
1
git cherry-pick 4c97ff3
回车之后,你的 feature-7 的代码就回来了。
期间可能会有一些冲突,按照提示解决就好。最后的结果如下图:

feature-1 到 feature-7 的代码就合并到了一起,以前的代码也都回来了。
自己总结
1
2
git remote add origin https://github.com/cenergy/test.git
git push -u origin master
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+git的操作记录 - AIGISSS git的操作记录
本文最后更新于:1 年前
与服务器上的代码产生冲突
如果系统中有一些配置文件在服务器上做了配置修改,然后后续开发又新添加一些配置项的时候,在发布这个配置文件的时候,会发生代码冲突:
error: Your local changes to the following files would be overwritten by merge:
protected/config/main.php
Please, commit your changes or stash them before you can merge.
如果希望保留生产服务器上所做的改动,仅仅并入新配置项, 处理方法如下:
1
2
3
git stash
git pull
git stash pop
然后可以使用 git diff -w +文件名
来确认代码自动合并的情况.
反过来,如果希望用代码库中的文件完全覆盖本地工作版本. 方法如下:
1
2
git reset --hard
git pull
其中 git reset
是针对版本,如果想针对文件回退本地修改,使用
1
git checkout HEAD file/to/restore
辛辛苦苦加班一星期敲的代码没了
过程是这样的,在终端输入 git log,列出所有的 commit 信息,如下图:

commit 的信息很简单,就是做了 6 个功能,每个功能对应一个 commit 的提交,分别是 feature-1 到 feature-6。
接下来执行了强制回滚,如下:
1
git reset --hard 2216d4e
回滚到了 feature-1 上,并且回滚的时候加了—hard,导致之前 feature-2 到 feature-6 的所有代码全部弄丢了,现在 git log 的显示如下:

现在 feature-2 到 feature-6 的代码没了。。。。。
然鹅还没完,在这个基础上新添加了一个 commit 提交,信息叫 feature-7,如下图:

现在 feature-2 到 feature-6 全没了,还多了一个 feature-7
请问 如何把丢失的代码 feature-2 到 feature-6 全部恢复回来,并且 feature-7 的代码也要保留
用 git reflog 和 git cherry-pick 就能解决
在终端里输入:
1
git reflog
然后就会展示出所有你之前 git 操作,你以前所有的操作都被 git 记录了下来,如下图:

这时候要记好两个值:4c97ff3 和 cd52afc,他们分别是 feature-7 和 feature-6 的 hash 码。然后执行回滚,回到 feature-6 上:
1
git reset --hard cd52afc
现在我们回到了 feature-6 上,如下图:

我们回到了 feature-6 上,但是 feature-7 没了,如何加上来呢?
这个时候就用上了 git cherry-pick,刚刚我们知道了 feature-7 的 hash 码为 4c97ff3,操作如下:
1
git cherry-pick 4c97ff3
回车之后,你的 feature-7 的代码就回来了。
期间可能会有一些冲突,按照提示解决就好。最后的结果如下图:

feature-1 到 feature-7 的代码就合并到了一起,以前的代码也都回来了。
自己总结
1
2
git remote add origin https://github.com/cenergy/test.git
git push -u origin master
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/d673e1b7.html b/posts/d673e1b7.html
index 6a36b10f..aa382f70 100644
--- a/posts/d673e1b7.html
+++ b/posts/d673e1b7.html
@@ -1 +1 @@
-23种经典的设计模式 - AIGISSS 23种经典的设计模式
本文最后更新于:9 天前
前言
23 种经典设计模式共分为 3 种类型,分别是创建型、结构型和行为型

创建型设计模式
创建型设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
单例模式
单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式
、懒汉式
、双重检测
、静态内部类
、枚举
。
尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用,主要的理由有以下几点:
- 单例对面向对象的特性支持不友好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数
那有什么替代单例的解决方案呢?如果要完全解决这些问题,我们可能要从根上寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC 容器来保证全局唯一性。
有人把单例当作反模式,主张杜绝在项目中使用。个人觉得这有点极端。模式本身没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局类,在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。
工厂模式
工厂模式包括简单工厂、工厂方法、抽象工厂这 3 种细分模式。其中,简单工厂和工厂方法比较常用,抽象工厂的应用场景比较特殊,所以很少用到。
工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。实际上,如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式。当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,我们推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
详细点说,工厂模式的作用有下面 4 个,这也是判断要不要使用工厂模式最本质的参考标准。
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
- 代码复用:创建代码抽离到独立的工厂类之后可以复用。
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
- 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
除此之外,我们还讲了工厂模式一个非常经典的应用场景:依赖注入框架
建造者模式
建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度使用。
- 如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
- 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
- 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型模式
原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。操作非常耗时的情况下,我们比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。
结构型设计模式
结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。
行为型设计模式
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题,行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+23种经典的设计模式 - AIGISSS 23种经典的设计模式
本文最后更新于:1 分钟前
前言
23 种经典设计模式共分为 3 种类型,分别是创建型、结构型和行为型

创建型设计模式
创建型设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
单例模式
单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式
、懒汉式
、双重检测
、静态内部类
、枚举
。
尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用,主要的理由有以下几点:
- 单例对面向对象的特性支持不友好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数
那有什么替代单例的解决方案呢?如果要完全解决这些问题,我们可能要从根上寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC 容器来保证全局唯一性。
有人把单例当作反模式,主张杜绝在项目中使用。个人觉得这有点极端。模式本身没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局类,在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。
工厂模式
工厂模式包括简单工厂、工厂方法、抽象工厂这 3 种细分模式。其中,简单工厂和工厂方法比较常用,抽象工厂的应用场景比较特殊,所以很少用到。
工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。实际上,如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式。当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,我们推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
详细点说,工厂模式的作用有下面 4 个,这也是判断要不要使用工厂模式最本质的参考标准。
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
- 代码复用:创建代码抽离到独立的工厂类之后可以复用。
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
- 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
除此之外,我们还讲了工厂模式一个非常经典的应用场景:依赖注入框架
建造者模式
建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度使用。
- 如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
- 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
- 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型模式
原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。操作非常耗时的情况下,我们比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。
结构型设计模式
结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。
行为型设计模式
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题,行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/db34898e.html b/posts/db34898e.html
index 8feb3cc1..edd44f00 100644
--- a/posts/db34898e.html
+++ b/posts/db34898e.html
@@ -1 +1 @@
-着色器之书02 - AIGISSS 着色器之书02
本文最后更新于:1 个月前
开始
什么是 Fragment Shader(片段着色器)?
在之前的章节我们把 shaders 和古腾堡印刷术相提并论。为什么这样类比呢?更重要的是,什么是 shader?

如果你曾经有用计算机绘图的经验,你就知道在这个过程中你需要画一个圆,然后一个长方形,一条线,一些三角形……直到画出你想要的图像。这个过程很像用手写一封信或一本书 —— 都是一系列的指令,需要你一件一件完成。
Shaders 也是一系列的指令,但是这些指令会对屏幕上的每个像素同时下达。也就是说,你的代码必须根据像素在屏幕上的不同位置执行不同的操作。就像活字印刷,你的程序就像一个 function(函数),输入位置信息,输出颜色信息,当它编译完之后会以相当快的速度运行。

为什么 shaders 运行特别快?
为了回答这个问题,不得不给大家介绍并行处理(parallel processing)的神奇之处。
想象你的 CPU 是一个大的工业管道,然后每一个任务都是通过这个管道的某些东西 —— 就像一个生产流水线那样。有些任务要比别的大,也就是说要花费更多时间和精力去处理。我们就称它要求更强的处理能力。由于计算机自身的架构,这些任务需要串行;即一次一个地依序完成。现代计算机通常有一组四个处理器,就像这个管道一样运行,一个接一个地处理这些任务,从而使计算机流畅运行。每个管道通常被称为线程。

视频游戏和其他图形应用比起别的程序来说,需要高得多的处理能力。因为它们的图形内容需要操作无数像素。想想看,屏幕上的每一个像素都需要计算,而在 3D 游戏中几何和透视也都需要计算。
让我们回到开始那个关于管道和任务的比喻。屏幕上的每个像素都代表一个最简单的任务。单独来看完成任何一个像素的任务对 CPU 来说都很容易,那么问题来了,屏幕上的每一个像素都需要解决这样的小任务!也就是说,哪怕是对于一个老式的屏幕(分辨率 800x600)来说,都需要每帧处理480000个像素,即每秒进行14400000次计算!是的,这对于微处理器就是大问题了!而对于一个现代的 2800x1800 视网膜屏,每秒运行60帧,就需要每秒进行311040000次计算。图形工程师是如何解决这个问题的?

这个时候,并行处理就是最好的解决方案。比起用三五个强大的微处理器(或者说“管道”)来处理这些信息,用一大堆小的微处理器来并行计算,就要好得多。这就是图形处理器(GPU : Graphic Processor Unit)的来由。

设想一堆小型微处理器排成一个平面的画面,假设每个像素的数据是乒乓球。14400000个乒乓球可以在一秒内阻塞几乎任何管道。但是一面800x600的管道墙,每秒接收30波480000个像素的信息就可以流畅完成。这在更高的分辨率下也是成立的 —— 并行的处理器越多,可以处理的数据流就越大。
另一个 GPU 的魔法是特殊数学函数可通过硬件加速。非常复杂的数学操作可以直接被微芯片解决,而无须通过软件。这就表示可以有更快的三角和矩阵运算 —— 和电流一样快。
GLSL是什么?
GLSL 代表 openGL Shading Language,openGL 着色语言,这是你在接下来章节看到的程序所遵循的具体标准。根据硬件和操作系统的不同,还有其他的着色器(shaders)。这里我们将依照Khronos Group的规则来执行。了解 OpenGL 的历史将有助于你理解大多数奇怪的约定,所以建议不妨阅读openglbook.com/chapter-0-preface-what-is-opengl.html。
为什么 Shaders 有名地不好学?
就像蜘蛛侠里的那句名言,能力越大责任越大,并行计算也是如此;GPU 的强大的架构设计也有其限制与不足。
为了能使许多管线并行运行,每一个线程必须与其他的相独立。我们称这些线程对于其他线程在进行的运算是“盲视”的。这个限制就会使得所有数据必须以相同的方向流动。所以就不可能检查其他线程的输出结果,修改输入的数据,或者把一个线程的输出结果输入给另一个线程。如果允许线程到线程的数据流动将使所有的数据面临威胁。
并且 GPU 会让所有并行的微处理器(管道们)一直处在忙碌状态;只要它们一有空闲就会接到新的信息。一个线程不可能知道它前一刻在做什么。它可能是在画操作系统界面上的一个按钮,然后渲染了游戏中的一部分天空,然后显示了一封 email 中的一些文字。每个线程不仅是“盲视”的,而且还是“无记忆”的。同时,它要求编写一个通用的规则,依据像素的不同位置依次输出不同的结果。这种抽象性,和盲视、无记忆的限制使得 shaders 在程序员新手中不是很受欢迎。
但是不要担心!在接下来的章节中,我们会一步一步地,由浅入深地学习着色语言。如果你是在用一个靠谱的浏览器阅读这个教程,你会喜欢边读边玩书中的示例的。好了,不要再浪费时间了,赶快去玩起来吧!
Hello World
“Hello world!”通常都是学习一个新语言的第一个例子。这是一个非常简单,只有一行的程序。它既是一个热情的欢迎,也传达了编程所能带来的可能性。
然而在 GPU 的世界里,第一步就渲染一行文字太难了,所以我们改为选择一个鲜艳的欢迎色,来吧躁起来!
如果你是在线阅读这本书的话,上面的代码都是可以交互的。你可以点击或者改动代码中任何一部分,尽情探索。多亏 GPU 的架构,shader 会飞速地编译和更新,这使得你的改动都会立刻出现在你眼前。试试改动第 6 行的值,看会发生什么。
尽管这几行简单的代码看起来不像有很多内容,我们还是可以据此推测出一些知识点:
shader 语言 有一个 main
函数,会在最后返回颜色值。这点和 C 语言很像。
最终的像素颜色取决于预设的全局变量 gl_FragColor
。
这个类 C 语言有内建的变量(像gl_FragColor
),函数和数据类型。在本例中我们刚刚介绍了vec4
(四分量浮点向量)。之后我们会见到更多的类型,像 vec3
(三分量浮点向量)和 vec2
(二分量浮点向量),还有非常著名的:float
(单精度浮点型), int
(整型) 和 bool
(布尔型)。
如果我们仔细观察 vec4
类型,可以推测这四个变元分别响应红,绿,蓝和透明度通道。同时我们也可以看到这些变量是规范化的,意思是它们的值是从 0 到 1 的。之后我们会学习如何规范化变量,使得在变量间map(映射)数值更加容易。
另一个可以从本例看出来的很重要的类 C 语言特征是,预处理程序的宏指令。宏指令是预编译的一部分。有了宏才可以 #define
(定义)全局变量和进行一些基础的条件运算(通过使用 #ifdef
和 #endif
)。所有的宏都以 #
开头。预编译会在编译前一刻发生,把所有的命令复制到 #defines
里,检查#ifdef
条件句是否已被定义, #ifndef
条件句是否没有被定义。在我们刚刚的“hello world!”的例子中,我们在第 2 行检查了 GL_ES
是否被定义,这个通常用在移动端或浏览器的编译中。
float
类型在 shaders 中非常重要,所以精度非常重要。更低的精度会有更快的渲染速度,但是会以质量为代价。你可以选择每一个浮点值的精度。在第一行(precision mediump float;
)我们就是设定了所有的浮点值都是中等精度。但我们也可以选择把这个值设为“低”(precision lowp float;
)或者“高”(precision highp float;
)。
最后可能也是最重要的细节是,GLSL 语言规范并不保证变量会被自动转换类别。这句话是什么意思呢?显卡的硬件制造商各有不同的显卡加速方式,但是却被要求有最精简的语言规范。因而,自动强制类型转换并没有包括在其中。在我们的“hello world!”例子中,vec4
精确到单精度浮点,所以应被赋予 float
格式。但是如果你想要代码前后一致,不要之后花费大量时间 debug 的话,最好养成在 float
型数值里加一个 .
的好习惯。如下这种代码就可能不能正常运行:
1
2
3
void main() {
gl_FragColor = vec4(1,0,0,1); // 出错
}
现在我们已经基本讨论完了“hello world!”例子中所有主要的内容,是时候点击代码,检验一下我们所学的知识了。你会发现出错时程序会编译失败,只留一个寂寞的白屏。你可以试试一些好玩的小点子,比如说:
把单精度浮点值换成整型数值,猜猜你的显卡能不能容忍这个行为。
试试把第六行注释掉,不给函数赋任何像素的值。
尝试另外写个函数,返回某个颜色,然后在 main()
里面使用这个函数。给个提示,这个函数应该长这样:
1
2
3
vec4 red(){
return vec4(1.0,0.0,0.0,1.0);
}
- 有很多种构造
vec4
类型的方式,试试看其他方式。下面就是其中一种方式:
1
vec4 color = vec4(vec3(1.0,0.0,1.0),1.0);
尽管这个例子看起来不那么刺激,它却是最最基础的 —— 我们把画布上的每一个像素都改成了一个确切的颜色。在接下来的章节中我们将会看到如何用两种输入源来改变像素的颜色:空间(依据像素在屏幕上的位置)和时间(依据页面加载了多少秒)。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书02 - AIGISSS 着色器之书02
本文最后更新于:1 个月前
开始
什么是 Fragment Shader(片段着色器)?
在之前的章节我们把 shaders 和古腾堡印刷术相提并论。为什么这样类比呢?更重要的是,什么是 shader?

如果你曾经有用计算机绘图的经验,你就知道在这个过程中你需要画一个圆,然后一个长方形,一条线,一些三角形……直到画出你想要的图像。这个过程很像用手写一封信或一本书 —— 都是一系列的指令,需要你一件一件完成。
Shaders 也是一系列的指令,但是这些指令会对屏幕上的每个像素同时下达。也就是说,你的代码必须根据像素在屏幕上的不同位置执行不同的操作。就像活字印刷,你的程序就像一个 function(函数),输入位置信息,输出颜色信息,当它编译完之后会以相当快的速度运行。

为什么 shaders 运行特别快?
为了回答这个问题,不得不给大家介绍并行处理(parallel processing)的神奇之处。
想象你的 CPU 是一个大的工业管道,然后每一个任务都是通过这个管道的某些东西 —— 就像一个生产流水线那样。有些任务要比别的大,也就是说要花费更多时间和精力去处理。我们就称它要求更强的处理能力。由于计算机自身的架构,这些任务需要串行;即一次一个地依序完成。现代计算机通常有一组四个处理器,就像这个管道一样运行,一个接一个地处理这些任务,从而使计算机流畅运行。每个管道通常被称为线程。

视频游戏和其他图形应用比起别的程序来说,需要高得多的处理能力。因为它们的图形内容需要操作无数像素。想想看,屏幕上的每一个像素都需要计算,而在 3D 游戏中几何和透视也都需要计算。
让我们回到开始那个关于管道和任务的比喻。屏幕上的每个像素都代表一个最简单的任务。单独来看完成任何一个像素的任务对 CPU 来说都很容易,那么问题来了,屏幕上的每一个像素都需要解决这样的小任务!也就是说,哪怕是对于一个老式的屏幕(分辨率 800x600)来说,都需要每帧处理480000个像素,即每秒进行14400000次计算!是的,这对于微处理器就是大问题了!而对于一个现代的 2800x1800 视网膜屏,每秒运行60帧,就需要每秒进行311040000次计算。图形工程师是如何解决这个问题的?

这个时候,并行处理就是最好的解决方案。比起用三五个强大的微处理器(或者说“管道”)来处理这些信息,用一大堆小的微处理器来并行计算,就要好得多。这就是图形处理器(GPU : Graphic Processor Unit)的来由。

设想一堆小型微处理器排成一个平面的画面,假设每个像素的数据是乒乓球。14400000个乒乓球可以在一秒内阻塞几乎任何管道。但是一面800x600的管道墙,每秒接收30波480000个像素的信息就可以流畅完成。这在更高的分辨率下也是成立的 —— 并行的处理器越多,可以处理的数据流就越大。
另一个 GPU 的魔法是特殊数学函数可通过硬件加速。非常复杂的数学操作可以直接被微芯片解决,而无须通过软件。这就表示可以有更快的三角和矩阵运算 —— 和电流一样快。
GLSL是什么?
GLSL 代表 openGL Shading Language,openGL 着色语言,这是你在接下来章节看到的程序所遵循的具体标准。根据硬件和操作系统的不同,还有其他的着色器(shaders)。这里我们将依照Khronos Group的规则来执行。了解 OpenGL 的历史将有助于你理解大多数奇怪的约定,所以建议不妨阅读openglbook.com/chapter-0-preface-what-is-opengl.html。
为什么 Shaders 有名地不好学?
就像蜘蛛侠里的那句名言,能力越大责任越大,并行计算也是如此;GPU 的强大的架构设计也有其限制与不足。
为了能使许多管线并行运行,每一个线程必须与其他的相独立。我们称这些线程对于其他线程在进行的运算是“盲视”的。这个限制就会使得所有数据必须以相同的方向流动。所以就不可能检查其他线程的输出结果,修改输入的数据,或者把一个线程的输出结果输入给另一个线程。如果允许线程到线程的数据流动将使所有的数据面临威胁。
并且 GPU 会让所有并行的微处理器(管道们)一直处在忙碌状态;只要它们一有空闲就会接到新的信息。一个线程不可能知道它前一刻在做什么。它可能是在画操作系统界面上的一个按钮,然后渲染了游戏中的一部分天空,然后显示了一封 email 中的一些文字。每个线程不仅是“盲视”的,而且还是“无记忆”的。同时,它要求编写一个通用的规则,依据像素的不同位置依次输出不同的结果。这种抽象性,和盲视、无记忆的限制使得 shaders 在程序员新手中不是很受欢迎。
但是不要担心!在接下来的章节中,我们会一步一步地,由浅入深地学习着色语言。如果你是在用一个靠谱的浏览器阅读这个教程,你会喜欢边读边玩书中的示例的。好了,不要再浪费时间了,赶快去玩起来吧!
Hello World
“Hello world!”通常都是学习一个新语言的第一个例子。这是一个非常简单,只有一行的程序。它既是一个热情的欢迎,也传达了编程所能带来的可能性。
然而在 GPU 的世界里,第一步就渲染一行文字太难了,所以我们改为选择一个鲜艳的欢迎色,来吧躁起来!
如果你是在线阅读这本书的话,上面的代码都是可以交互的。你可以点击或者改动代码中任何一部分,尽情探索。多亏 GPU 的架构,shader 会飞速地编译和更新,这使得你的改动都会立刻出现在你眼前。试试改动第 6 行的值,看会发生什么。
尽管这几行简单的代码看起来不像有很多内容,我们还是可以据此推测出一些知识点:
shader 语言 有一个 main
函数,会在最后返回颜色值。这点和 C 语言很像。
最终的像素颜色取决于预设的全局变量 gl_FragColor
。
这个类 C 语言有内建的变量(像gl_FragColor
),函数和数据类型。在本例中我们刚刚介绍了vec4
(四分量浮点向量)。之后我们会见到更多的类型,像 vec3
(三分量浮点向量)和 vec2
(二分量浮点向量),还有非常著名的:float
(单精度浮点型), int
(整型) 和 bool
(布尔型)。
如果我们仔细观察 vec4
类型,可以推测这四个变元分别响应红,绿,蓝和透明度通道。同时我们也可以看到这些变量是规范化的,意思是它们的值是从 0 到 1 的。之后我们会学习如何规范化变量,使得在变量间map(映射)数值更加容易。
另一个可以从本例看出来的很重要的类 C 语言特征是,预处理程序的宏指令。宏指令是预编译的一部分。有了宏才可以 #define
(定义)全局变量和进行一些基础的条件运算(通过使用 #ifdef
和 #endif
)。所有的宏都以 #
开头。预编译会在编译前一刻发生,把所有的命令复制到 #defines
里,检查#ifdef
条件句是否已被定义, #ifndef
条件句是否没有被定义。在我们刚刚的“hello world!”的例子中,我们在第 2 行检查了 GL_ES
是否被定义,这个通常用在移动端或浏览器的编译中。
float
类型在 shaders 中非常重要,所以精度非常重要。更低的精度会有更快的渲染速度,但是会以质量为代价。你可以选择每一个浮点值的精度。在第一行(precision mediump float;
)我们就是设定了所有的浮点值都是中等精度。但我们也可以选择把这个值设为“低”(precision lowp float;
)或者“高”(precision highp float;
)。
最后可能也是最重要的细节是,GLSL 语言规范并不保证变量会被自动转换类别。这句话是什么意思呢?显卡的硬件制造商各有不同的显卡加速方式,但是却被要求有最精简的语言规范。因而,自动强制类型转换并没有包括在其中。在我们的“hello world!”例子中,vec4
精确到单精度浮点,所以应被赋予 float
格式。但是如果你想要代码前后一致,不要之后花费大量时间 debug 的话,最好养成在 float
型数值里加一个 .
的好习惯。如下这种代码就可能不能正常运行:
1
2
3
void main() {
gl_FragColor = vec4(1,0,0,1); // 出错
}
现在我们已经基本讨论完了“hello world!”例子中所有主要的内容,是时候点击代码,检验一下我们所学的知识了。你会发现出错时程序会编译失败,只留一个寂寞的白屏。你可以试试一些好玩的小点子,比如说:
把单精度浮点值换成整型数值,猜猜你的显卡能不能容忍这个行为。
试试把第六行注释掉,不给函数赋任何像素的值。
尝试另外写个函数,返回某个颜色,然后在 main()
里面使用这个函数。给个提示,这个函数应该长这样:
1
2
3
vec4 red(){
return vec4(1.0,0.0,0.0,1.0);
}
- 有很多种构造
vec4
类型的方式,试试看其他方式。下面就是其中一种方式:
1
vec4 color = vec4(vec3(1.0,0.0,1.0),1.0);
尽管这个例子看起来不那么刺激,它却是最最基础的 —— 我们把画布上的每一个像素都改成了一个确切的颜色。在接下来的章节中我们将会看到如何用两种输入源来改变像素的颜色:空间(依据像素在屏幕上的位置)和时间(依据页面加载了多少秒)。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/dc594d97.html b/posts/dc594d97.html
index 11a91ee8..44a8290e 100644
--- a/posts/dc594d97.html
+++ b/posts/dc594d97.html
@@ -1 +1 @@
-着色器之书06 - AIGISSS 着色器之书06
本文最后更新于:1 个月前
颜色

我们目前为止还未涉及到GLSL的向量类型。在我们深入向量之前,学习更多关于变量和色彩主题是一个了解向量类型的好方法。
若你熟悉面向对象的编程范式(或者说编程思维模式),你一定注意到我们以一种类C的 struct
的方式访问向量数据的内部分量。
1
2
3
4
vec3 red = vec3(1.0,0.0,0.0);
red.x = 1.0;
red.y = 0.0;
red.z = 0.0;
以x,y,z定义颜色是不是有些奇怪?正因如此,我们有其他方法访问这些变量——以不同的名字。.x
, .y
, .z
也可以被写作.r
, .g
, .b
和 .s
, .t
, .p
。(.s
, .t
, .p
通常被用做后面章节提到的贴图空间坐标)你也可以通过使用索引位置[0]
, [1]
和 [2]
来访问向量.
下面的代码展示了所有访问相同数据的方式:
1
2
3
4
5
vec4 vector;
vector[0] = vector.r = vector.x = vector.s;
vector[1] = vector.g = vector.y = vector.t;
vector[2] = vector.b = vector.z = vector.p;
vector[3] = vector.a = vector.w = vector.q;
这些指向向量内部变量的不同方式仅仅是设计用来帮助你写出干净代码的术语。着色语言所包含的灵活性为你互换地思考颜色和坐标位置。
GLSL中向量类型的另一大特点是可以用你需要的任意顺序简单地投射和混合(变量)值。这种能力被(形象地)称为:鸡尾酒。
1
2
3
4
5
6
7
8
9
10
11
vec3 yellow, magenta, green;
// Making Yellow
yellow.rg = vec2(1.0); // Assigning 1. to red and green channels
yellow[2] = 0.0; // Assigning 0. to blue channel
// Making Magenta
magenta = yellow.rbg; // Assign the channels with green and blue swapped
// Making Green
green.rgb = yellow.bgb; // Assign the blue channel of Yellow (0) to red and blue channels
个人工具箱
你可能不习惯用数字拾取颜色—这样非常反直觉。幸运的是,(app store上)有许多可以轻松完成这项任务的程序。寻找一个合适自己的并练习将颜色转化为 vec3
或 vec4
格式。例如,这是我在Spectrum中使用的的模板。
混合颜色
现在你了解到如何定义颜色,是时候将先前所学的整合一下了!在GLSL中,有个十分有用的函数:mix()
,这个函数让你以百分比混合两个值。猜下百分比的取值范围?没错,0到1!完美!学了这么久的基本功,是时候来用一用了!

看下下列代码中的第18行,这里展示了我们如果是用随时间变化的sin绝对值来混合 colorA
和 colorB
。
试着来 show 一下你所学到的:
- 给颜色赋予一个有趣的过渡。想想某种特定的感情。哪种颜色更具代表性?他如何产生?又如何褪去?再想想另外的一种感情以及对应的颜色。然后改变上诉代码中的代表这种情感的开始颜色和结束颜色。Robert Penner 开发了一些列流行的计算机动画塑形函数,被称为缓动函数。你可以研究这些例子并得到启发,但最好你还是自己写一个自己的缓动函数。
玩玩渐变
mix()
函数有更多的用处。我们可以输入两个互相匹配的变量类型而不仅仅是单独的 float
变量,在我们这个例子中用的是 vec3
。这样我们便获得了混合颜色单独通道 .r
,.g
和 .b
的能力。

试试下面的例子。正如前面一个例子,我们用一条线来可视化根据单位化x坐标的过渡。现在所有通道都按照同样的线性变换过渡。
现在试试取消25行的注释,看看会发生什么。然后再试试取消26行和27行。记住直线代表了colorA
和 colorB
每个通道的混合比例。
你可能认出了我们用在25行到27行的造型函数。试着改写他们!是时候把前几张的内容结合起来探索一些新的渐变。试试下列挑战:

创作一个渐变来代表 William Turner的落日。
用 u_time
做个一日出和日落的动画。
能用我们所学的做一道彩虹吗?
用 step()
函数在做一个五彩的旗子。
HSB
我们不能脱离色彩空间来谈论颜色。正如你所知,除了rgb值,有其他不同的方法去描述定义颜色。
HSB 代表色相,饱和度和亮度(或称为值)。这更符合直觉也更有利于组织颜色。稍微花些时间阅读下面的 rgb2hsv()
和 hsv2rgb()
函数。
将x坐标(位置)映射到Hue值并将y坐标映射到明度,我们就得到了五彩的可见光光谱。这样的色彩空间分布实现起来非常方便,比起RGB,用HSB来拾取颜色更直观。
极坐标下的HSB
HSB原本是在极坐标下产生的(以半径和角度定义)而并非在笛卡尔坐标系(基于xy定义)下。将HSB映射到极坐标我们需要取得角度和到像素屏中点的距离。由此我们运用 length()
函数和 atan(y,x)
函数(在GLSL中通常用atan(y,x))。
当用到矢量和三角学函数时,vec2
, vec3
和 vec4
被当做向量对待,即使有时候他们代表颜色。我们开始把颜色和向量同等的对待,事实上你会慢慢发现这种理念的灵活性有着相当强大的用途。
注意:如果你想了解,除length()以外的诸多几何函数,例如:distance()
, dot()
, cross
, normalize()
, faceforward()
, reflect()
和 refract()
。 GLSL也有与向量相关的函数:lessThan()
, lessThanEqual()
, greaterThan()
, greaterThanEqual()
, equal()
and notEqual()
。
一旦我们得到角度和长度,我们需要单位化这些值:0.0到1.0。在27行,atan(y,x)
会返回一个介于-PI到PI的弧度值(-3.14 to 3.14),所以我们要将这个返回值除以 TWO_PI
(在code顶部定义了)来得到一个-0.5到0.5的值。这样一来,用简单的加法就可以把这个返回值最终映射到0.0到1.0。半径会返回一个最大值0.5(因为我们计算的是到视口中心的距离,而视口中心的范围已经被映射到0.0到1.0),所以我们需要把这个值乘以二来得到一个0到1.0的映射。
正如你所见,这里我们的游戏都是关于变换和映射到一个0到1这样我们乐于处理的值。
来挑战下下面的练习吧:
把极坐标映射的例子改成选择色轮,就像“正忙”的鼠标图标。
把造型函数整合进来,来让HSB和RGB的转换中强调某些特定值并且弱化其他的。

- 如果你仔细观察用来拾色的色轮(见下图),你会发现它用一种根据RYB色彩空间的色谱。例如,红色的对面应该是绿色,但在我们的例子里是青色。你能找到一种修复的方式来让它看起来和下图一样么?[提示:这是用塑形函数的好机会!]

注意函数和变量
在进入下一章之前让我们停下脚步回顾下。复习下之前例子的函数。你会注意到变量类型之前有个限定符 in
,在这个 qualifier (限定符)例子中它特指这个变量是只读的。在之后的例子中我们会看到可以定义一个 out
或者 inout
变量。最后这个 inout
,再概念上类似于参照输入一个变量,这意味着我们有可能修改一个传入的变量。
1
2
3
int newFunction(in vec4 aVec4, // read-only
out vec3 aVec3, // write-only
inout int aInt); // read-write
或许你还不相信我们可以用所有这些元素来画一些炫酷的东西。下一章我们会学习如何结合所有这些技巧通过融合 (blending) 空间来创造几何形状。没错。。。融合(blending) 空间。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+着色器之书06 - AIGISSS 着色器之书06
本文最后更新于:1 个月前
颜色

我们目前为止还未涉及到GLSL的向量类型。在我们深入向量之前,学习更多关于变量和色彩主题是一个了解向量类型的好方法。
若你熟悉面向对象的编程范式(或者说编程思维模式),你一定注意到我们以一种类C的 struct
的方式访问向量数据的内部分量。
1
2
3
4
vec3 red = vec3(1.0,0.0,0.0);
red.x = 1.0;
red.y = 0.0;
red.z = 0.0;
以x,y,z定义颜色是不是有些奇怪?正因如此,我们有其他方法访问这些变量——以不同的名字。.x
, .y
, .z
也可以被写作.r
, .g
, .b
和 .s
, .t
, .p
。(.s
, .t
, .p
通常被用做后面章节提到的贴图空间坐标)你也可以通过使用索引位置[0]
, [1]
和 [2]
来访问向量.
下面的代码展示了所有访问相同数据的方式:
1
2
3
4
5
vec4 vector;
vector[0] = vector.r = vector.x = vector.s;
vector[1] = vector.g = vector.y = vector.t;
vector[2] = vector.b = vector.z = vector.p;
vector[3] = vector.a = vector.w = vector.q;
这些指向向量内部变量的不同方式仅仅是设计用来帮助你写出干净代码的术语。着色语言所包含的灵活性为你互换地思考颜色和坐标位置。
GLSL中向量类型的另一大特点是可以用你需要的任意顺序简单地投射和混合(变量)值。这种能力被(形象地)称为:鸡尾酒。
1
2
3
4
5
6
7
8
9
10
11
vec3 yellow, magenta, green;
// Making Yellow
yellow.rg = vec2(1.0); // Assigning 1. to red and green channels
yellow[2] = 0.0; // Assigning 0. to blue channel
// Making Magenta
magenta = yellow.rbg; // Assign the channels with green and blue swapped
// Making Green
green.rgb = yellow.bgb; // Assign the blue channel of Yellow (0) to red and blue channels
个人工具箱
你可能不习惯用数字拾取颜色—这样非常反直觉。幸运的是,(app store上)有许多可以轻松完成这项任务的程序。寻找一个合适自己的并练习将颜色转化为 vec3
或 vec4
格式。例如,这是我在Spectrum中使用的的模板。
混合颜色
现在你了解到如何定义颜色,是时候将先前所学的整合一下了!在GLSL中,有个十分有用的函数:mix()
,这个函数让你以百分比混合两个值。猜下百分比的取值范围?没错,0到1!完美!学了这么久的基本功,是时候来用一用了!

看下下列代码中的第18行,这里展示了我们如果是用随时间变化的sin绝对值来混合 colorA
和 colorB
。
试着来 show 一下你所学到的:
- 给颜色赋予一个有趣的过渡。想想某种特定的感情。哪种颜色更具代表性?他如何产生?又如何褪去?再想想另外的一种感情以及对应的颜色。然后改变上诉代码中的代表这种情感的开始颜色和结束颜色。Robert Penner 开发了一些列流行的计算机动画塑形函数,被称为缓动函数。你可以研究这些例子并得到启发,但最好你还是自己写一个自己的缓动函数。
玩玩渐变
mix()
函数有更多的用处。我们可以输入两个互相匹配的变量类型而不仅仅是单独的 float
变量,在我们这个例子中用的是 vec3
。这样我们便获得了混合颜色单独通道 .r
,.g
和 .b
的能力。

试试下面的例子。正如前面一个例子,我们用一条线来可视化根据单位化x坐标的过渡。现在所有通道都按照同样的线性变换过渡。
现在试试取消25行的注释,看看会发生什么。然后再试试取消26行和27行。记住直线代表了colorA
和 colorB
每个通道的混合比例。
你可能认出了我们用在25行到27行的造型函数。试着改写他们!是时候把前几张的内容结合起来探索一些新的渐变。试试下列挑战:

创作一个渐变来代表 William Turner的落日。
用 u_time
做个一日出和日落的动画。
能用我们所学的做一道彩虹吗?
用 step()
函数在做一个五彩的旗子。
HSB
我们不能脱离色彩空间来谈论颜色。正如你所知,除了rgb值,有其他不同的方法去描述定义颜色。
HSB 代表色相,饱和度和亮度(或称为值)。这更符合直觉也更有利于组织颜色。稍微花些时间阅读下面的 rgb2hsv()
和 hsv2rgb()
函数。
将x坐标(位置)映射到Hue值并将y坐标映射到明度,我们就得到了五彩的可见光光谱。这样的色彩空间分布实现起来非常方便,比起RGB,用HSB来拾取颜色更直观。
极坐标下的HSB
HSB原本是在极坐标下产生的(以半径和角度定义)而并非在笛卡尔坐标系(基于xy定义)下。将HSB映射到极坐标我们需要取得角度和到像素屏中点的距离。由此我们运用 length()
函数和 atan(y,x)
函数(在GLSL中通常用atan(y,x))。
当用到矢量和三角学函数时,vec2
, vec3
和 vec4
被当做向量对待,即使有时候他们代表颜色。我们开始把颜色和向量同等的对待,事实上你会慢慢发现这种理念的灵活性有着相当强大的用途。
注意:如果你想了解,除length()以外的诸多几何函数,例如:distance()
, dot()
, cross
, normalize()
, faceforward()
, reflect()
和 refract()
。 GLSL也有与向量相关的函数:lessThan()
, lessThanEqual()
, greaterThan()
, greaterThanEqual()
, equal()
and notEqual()
。
一旦我们得到角度和长度,我们需要单位化这些值:0.0到1.0。在27行,atan(y,x)
会返回一个介于-PI到PI的弧度值(-3.14 to 3.14),所以我们要将这个返回值除以 TWO_PI
(在code顶部定义了)来得到一个-0.5到0.5的值。这样一来,用简单的加法就可以把这个返回值最终映射到0.0到1.0。半径会返回一个最大值0.5(因为我们计算的是到视口中心的距离,而视口中心的范围已经被映射到0.0到1.0),所以我们需要把这个值乘以二来得到一个0到1.0的映射。
正如你所见,这里我们的游戏都是关于变换和映射到一个0到1这样我们乐于处理的值。
来挑战下下面的练习吧:
把极坐标映射的例子改成选择色轮,就像“正忙”的鼠标图标。
把造型函数整合进来,来让HSB和RGB的转换中强调某些特定值并且弱化其他的。

- 如果你仔细观察用来拾色的色轮(见下图),你会发现它用一种根据RYB色彩空间的色谱。例如,红色的对面应该是绿色,但在我们的例子里是青色。你能找到一种修复的方式来让它看起来和下图一样么?[提示:这是用塑形函数的好机会!]

注意函数和变量
在进入下一章之前让我们停下脚步回顾下。复习下之前例子的函数。你会注意到变量类型之前有个限定符 in
,在这个 qualifier (限定符)例子中它特指这个变量是只读的。在之后的例子中我们会看到可以定义一个 out
或者 inout
变量。最后这个 inout
,再概念上类似于参照输入一个变量,这意味着我们有可能修改一个传入的变量。
1
2
3
int newFunction(in vec4 aVec4, // read-only
out vec3 aVec3, // write-only
inout int aInt); // read-write
或许你还不相信我们可以用所有这些元素来画一些炫酷的东西。下一章我们会学习如何结合所有这些技巧通过融合 (blending) 空间来创造几何形状。没错。。。融合(blending) 空间。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/e1a3a842.html b/posts/e1a3a842.html
index 3f2c07b8..80702cb7 100644
--- a/posts/e1a3a842.html
+++ b/posts/e1a3a842.html
@@ -1 +1 @@
-Mapbox加载空白地图 - AIGISSS Mapbox加载空白地图
本文最后更新于:8 个月前
由于 mapbox 的服务器在国外,在开发的时候有可能加载很慢,而且大多时候与背景地图无关,此时可以加载一个空白的地图来增加加载速度从而提高开发效率。
代码如下:
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Add an image</title>
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const blankStyle = {
version: 8,
name: "BlankMap",
sources: {},
layers: [
{
id: 'background',
type: 'background',
paint: { 'background-color': '#08294A' } /* 背景颜色 */
}
]
};
var map = new mapboxgl.Map({
container: "map",
zoom: 3,
center: [0, 0],
style: blankStyle
});
</script>
</body>
</html>
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+Mapbox加载空白地图 - AIGISSS Mapbox加载空白地图
本文最后更新于:8 个月前
由于 mapbox 的服务器在国外,在开发的时候有可能加载很慢,而且大多时候与背景地图无关,此时可以加载一个空白的地图来增加加载速度从而提高开发效率。
代码如下:
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Add an image</title>
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const blankStyle = {
version: 8,
name: "BlankMap",
sources: {},
layers: [
{
id: 'background',
type: 'background',
paint: { 'background-color': '#08294A' } /* 背景颜色 */
}
]
};
var map = new mapboxgl.Map({
container: "map",
zoom: 3,
center: [0, 0],
style: blankStyle
});
</script>
</body>
</html>
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/e6c6b2b1.html b/posts/e6c6b2b1.html
index f7fad44c..ac63d788 100644
--- a/posts/e6c6b2b1.html
+++ b/posts/e6c6b2b1.html
@@ -1 +1 @@
-WebGL编程指南学习笔记 - AIGISSS WebGL编程指南学习笔记
本文最后更新于:1 年前
个人计算机上使用最广泛的两种三维图形渲染技术是Direct3D和OpenGL。
Direct3D 是微软 DirectX 技术的一部分,是一套由微软控制的编程接口 API,主要用在 Windows 平台。
OpenGL 由于其开发和免费的特性,在多种平台上都有广泛的使用。
WebGL 是基于 OpenGL ES 2.0 的。
OpenGL、OpenGL ES 1.1/2.0/3.0 和 WebGL 的关系。

从 2.0 版本开始,OpenGL 支持了一项非常重要的特性,即可编程着色器方法。该特性被 OpenGL ES 2.0 继承,并成为了 WebGL 1.0 标准的核心部分。
着色器,使用一种类似于 C 的编程语言实现了精美的视觉效果。编写着色器的语言又称为着色器语言。WebGL 基于 OpenGL ES 2.0,使用 GLSL ES 语言编写着色器
下图显示了 WebGL 程序的结构:

WebGL 需要两种着色器:
顶点着色器( Vertex shader )
顶点着色器是用来描述顶点特性(如位置、颜色等)的程序。顶点(vertex)是指二维或三维空间中的一个点,如二维或三维图形的端点或交点。
片元着色器(Fragment shader)
片元着色器是进行逐片元处理过程如光照的程序。片元(fragment)是一个 WebGL 术语,你可以将其理解为像素(图像的单元)。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+WebGL编程指南学习笔记 - AIGISSS WebGL编程指南学习笔记
本文最后更新于:1 分钟前
个人计算机上使用最广泛的两种三维图形渲染技术是Direct3D和OpenGL。
Direct3D 是微软 DirectX 技术的一部分,是一套由微软控制的编程接口 API,主要用在 Windows 平台。
OpenGL 由于其开发和免费的特性,在多种平台上都有广泛的使用。
WebGL 是基于 OpenGL ES 2.0 的。
OpenGL、OpenGL ES 1.1/2.0/3.0 和 WebGL 的关系。

从 2.0 版本开始,OpenGL 支持了一项非常重要的特性,即可编程着色器方法。该特性被 OpenGL ES 2.0 继承,并成为了 WebGL 1.0 标准的核心部分。
着色器,使用一种类似于 C 的编程语言实现了精美的视觉效果。编写着色器的语言又称为着色器语言。WebGL 基于 OpenGL ES 2.0,使用 GLSL ES 语言编写着色器
下图显示了 WebGL 程序的结构:

WebGL 需要两种着色器:
顶点着色器( Vertex shader )
顶点着色器是用来描述顶点特性(如位置、颜色等)的程序。顶点(vertex)是指二维或三维空间中的一个点,如二维或三维图形的端点或交点。
片元着色器(Fragment shader)
片元着色器是进行逐片元处理过程如光照的程序。片元(fragment)是一个 WebGL 术语,你可以将其理解为像素(图像的单元)。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/eba3f111.html b/posts/eba3f111.html
index 3ae0af21..ef5ff07c 100644
--- a/posts/eba3f111.html
+++ b/posts/eba3f111.html
@@ -1,4 +1,4 @@
-使用Hexo + NexT 快速搭建博客 - AIGISSS 使用Hexo + NexT 快速搭建博客
本文最后更新于:1 年前
安装
安装 node.js
如果你已经安装了 node.js,请忽略。
访问node.js 官网,根据指引进行安装。
安装 Git
如果你已经安装了 Git,请忽略。
访问Git 官网,根据指引进行安装。
由于众所周知的原因,Windows 从上面的链接下载 git for windows 最好挂上一个代理,否则下载速度十分缓慢。也可以参考这个页面,收录了存储于百度云的下载地址。
安装 Hexo
国内的朋友,因为众所周知的原因,从 npm 直接安装 hexo 会非常慢,所以你需要用到镜像源,参考上面的步骤,使用 cnpm 命令行工具代替默认的 npm: 在 windows 控制台(cmd)里输入并执行npm install -g cnpm --registry=https://registry.npm.taobao.org
,然后安装 hexo: cnpm install -g hexo-cli
国外的朋友,请直接打开 windows 控制台,输入npm install -g hexo-cli
并执行。
建站
建立本地博客文件夹
在命令行执行如下命令,其中<folder>
为文件夹路径
1
2
hexo init <folder>
cd <folder>
所有有关hexo
的命令 均要在<folder>
路径下执行。
建立好后文件夹目录如下
1
2
3
4
5
6
7
8
9
.
├── _config.yml
├── package.json
├── .gitignore
├── node_modules
├── scaffolds
├── source
| ├── _posts
└── themes
其中
_config.yml
:站点的配置文件,可以在此配置大部分的参数。
package.json
:应用程序的信息。EJS, Stylus 和 Markdown renderer 已默认安装,您可以自由移除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "hexo-site",
"version": "0.0.0",
"private": true,
"hexo": {
"version": "3.9.0"
},
"dependencies": {
"version": "3.9.0"
"hexo-generator-archive": "^0.1.5",
"hexo-generator-category": "^0.1.3",
"hexo-generator-index": "^0.2.1",
"hexo-generator-tag": "^0.2.0",
"hexo-renderer-ejs": "^0.3.1",
"hexo-renderer-stylus": "^0.3.3",
"hexo-renderer-marked": "^1.0.1",
"hexo-server": "^0.3.3"
}
scaffolds:模板文件夹,是指在新建的文章文件中默认填充的内容。例如,如果您修改scaffold/post.md中的Front-matter内容,那么每次新建一篇文章时都会包含这个修改。
source:资源文件夹,存放用户资源的地方。除_posts
文件夹之外,开头命名为 _ (下划线)的文件/文件夹和隐藏的文件将会被忽略。Markdown 和 HTML 文件会被解析并放到 public 文件夹,而其他文件会被拷贝过去。
themes:主题文件夹。Hexo 会根据主题来生成静态页面。
node_modules:node.js 模块,一些 插件 和 依赖 会被安装到这里。
更加详细的解释请参考hexo 官方文档
安装 NexT 主题
进入本地博客文件夹并将 NexT 主题clone
至themes
文件夹下
1
git clone https://github.com/theme-next/hexo-theme-next themes/next
你会看到,在next
下也有一个_config.yml
的文件,这是 NexT 主题的配置文件,为了区别它和 博客配置文件,下面会用带路径的文件名来描述它们:
<folder>/_config.yml
:站点配置文件next/_config.yml
:主题配置文件
启用 NexT 主题
在<folder>/_config.yml
里theme:
选项填next
,=>theme: next
,注意冒号后空一格。
到这里,建站的任务就完成了。你现在可以打开控制台,输入并执行如下命令:
1
hexo g
完成没有报错之后执行如下命令:
1
hexo s
其中
hexo g
:新建public
文件夹,并在其中生成网站静态文件(html,css,等文件)hexo s
:启动 hexo 服务器,默认情况下,访问网址为:http://localhost:4000/
你最后会看到控制台有如下输出:
1
INFO Hexo is running at http://localhost:4000/. Press Ctrl+C to stop.
在浏览器地址栏输入http://localhost:4000/
并访问,你应该会看到如下页面:

**恭喜你!你已经完成了博客搭建的主要工作!接下来就是细节的配置了。请耐心阅读以下内容。**配置
网站脚注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
footer:
#建站时间
since: 2018
#作者头像并且是动画效果
icon:
name: user
animated: true
color: "##66CDAA"
#显示版权作者
copyright: aigisss 爱即是诗
#不显示Hexo
powered:
enable: false
version: false
#不显示主题和版本
theme:
enable: false
version: false
#显示备案号
beian:
enable: true
icp: 赣ICP备18013338-1号
版权声明
1
2
3
4
creative_commons:
license: by-nc-sa
sidebar: false
post: true
代码块
1
2
3
4
5
6
7
8
9
codeblock:
# 自定义边框半径,默认是1
# 值越大弧度越大
border_radius: 6
# 右上角显示复制按钮
copy_button:
enable: true
# 显示复制结果
show_result: true
分享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
needmoreshare2:
enable: true
postbottom:
#文章底部
enable: false
options:
iconStyle: box
boxForm: horizontal
position: bottomCenter
networks: Weibo,Wechat,Douban,QQZone,Twitter,Facebook
#左下角悬浮按钮
float:
enable: true
options:
iconStyle: box
boxForm: horizontal
position: middleRight
networks: Weibo,Wechat,Douban,QQZone,Twitter,Facebook
访问次数
1
2
3
4
5
6
7
8
9
10
11
12
# busuanzi统计
busuanzi_count:
enable: true
# 总访客数
total_visitors: true
total_visitors_icon: user
# 总浏览量
total_views: true
total_views_icon: eye
# 文章浏览量
post_views: true
post_views_icon: eye
顶部阅读进度条
1
2
3
4
reading_progress:
enable: true
color: "#37c6c0"
height: 2px
加载动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
motion:
# 启用
enable: true
# 异步加载
async: true
transition:
# Transition variants:
# fadeIn | fadeOut | flipXIn | flipXOut | flipYIn | flipYOut | flipBounceXIn | flipBounceXOut | flipBounceYIn | flipBounceYOut
# swoopIn | swoopOut | whirlIn | whirlOut | shrinkIn | shrinkOut | expandIn | expandOut
# bounceIn | bounceOut | bounceUpIn | bounceUpOut | bounceDownIn | bounceDownOut | bounceLeftIn | bounceLeftOut | bounceRightIn | bounceRightOut
# slideUpIn | slideUpOut | slideDownIn | slideDownOut | slideLeftIn | slideLeftOut | slideRightIn | slideRightOut
# slideUpBigIn | slideUpBigOut | slideDownBigIn | slideDownBigOut | slideLeftBigIn | slideLeftBigOut | slideRightBigIn | slideRightBigOut
# perspectiveUpIn | perspectiveUpOut | perspectiveDownIn | perspectiveDownOut | perspectiveLeftIn | perspectiveLeftOut | perspectiveRightIn | perspectiveRightOut
# 文章摘要动画
post_block: bounceIn
# 加载各种页面动画(分类,关于,标签等等)
post_header: fadeIn
# 文章详情动画
post_body: fadeIn
#
coll_header: fadeIn
# Only for Pisces | Gemini.
# 侧边栏(人物头像的那部分)
sidebar: fadeIn
搜索功能
NexT
自带提供了两个搜索
algolia_search
local_search
其实这个local_search
已经很好用了,配置algolia_search
挺麻烦的,而且搜索功能也用的不多
毕竟有万能的Ctrl + F
1
2
3
4
5
6
7
8
9
local_search:
enable: true
# if auto, trigger search by changing input
# if manual, trigger search by pressing enter key or search button
trigger: auto
# show top n results per article, show all results by setting to -1
top_n_per_article: 1
# unescape html strings to the readable one
unescape: false
添加 RSS 订阅
1
2
3
4
5
6
7
8
9
10
npm install hexo-generator-feed --save
复制代码
# Extensions
plugins:
hexo-generate-feed
feed: # RSS订阅插件
type: atom
path: atom.xml
limit: 0 #0就是代表所有
数学公式
1
2
3
4
5
6
7
8
9
10
11
# Math Equations Render Support
math:
enable: true
# Default(true) will load mathjax/katex script on demand
# That is it only render those page who has 'mathjax: true' in Front Matter.
# If you set it to false, it will load mathjax/katex srcipt EVERY PAGE.
per_page: true
engine: mathjax
#engine: katex
还需要在文章的 Front-matter 里打开 mathjax 开关,比如:
1
2
3
4
5
title: 使用hexo下next主题搭建博客的记录
tags: 日常
abbrlink: 326cb881
date: 2019-09-09 09:58:21
mathjax: true
网上一大堆说会出现语义冲突——-类 Latex 格式书写的数学公式下划线_
表示下标,有特殊的含义,如果被强制转换为<em>
标签,那么 MathJax 引擎在渲染数学公式的时候就会出错。类似的语义冲突的符号还包括*
, {
, }
, \\
等。但是!!
在我试验下没有出现此类问题,只要在主题中打开,md 中申明 mathjax: true 就好了,可能在我使用的next6.7
中解决了冲突。比如以下的公式能出来!
1
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
1
2
3
4
5
6
7
8
$$
P = \frac
{\sum_{i=1}^n (x_i- x)(y_i- y)}
{\displaystyle \left[
\sum_{i=1}^n (x_i-x)^2
\sum_{i=1}^n (y_i-y)^2
\right]^{1/2} }
$$
使用Hexo + NexT 快速搭建博客
本文最后更新于:1 年前
安装
安装 node.js
如果你已经安装了 node.js,请忽略。
访问node.js 官网,根据指引进行安装。
安装 Git
如果你已经安装了 Git,请忽略。
访问Git 官网,根据指引进行安装。
由于众所周知的原因,Windows 从上面的链接下载 git for windows 最好挂上一个代理,否则下载速度十分缓慢。也可以参考这个页面,收录了存储于百度云的下载地址。
安装 Hexo
国内的朋友,因为众所周知的原因,从 npm 直接安装 hexo 会非常慢,所以你需要用到镜像源,参考上面的步骤,使用 cnpm 命令行工具代替默认的 npm: 在 windows 控制台(cmd)里输入并执行npm install -g cnpm --registry=https://registry.npm.taobao.org
,然后安装 hexo: cnpm install -g hexo-cli
国外的朋友,请直接打开 windows 控制台,输入npm install -g hexo-cli
并执行。
建站
建立本地博客文件夹
在命令行执行如下命令,其中<folder>
为文件夹路径
1
2
hexo init <folder>
cd <folder>
所有有关hexo
的命令 均要在<folder>
路径下执行。
建立好后文件夹目录如下
1
2
3
4
5
6
7
8
9
.
├── _config.yml
├── package.json
├── .gitignore
├── node_modules
├── scaffolds
├── source
| ├── _posts
└── themes
其中
_config.yml
:站点的配置文件,可以在此配置大部分的参数。
package.json
:应用程序的信息。EJS, Stylus 和 Markdown renderer 已默认安装,您可以自由移除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "hexo-site",
"version": "0.0.0",
"private": true,
"hexo": {
"version": "3.9.0"
},
"dependencies": {
"version": "3.9.0"
"hexo-generator-archive": "^0.1.5",
"hexo-generator-category": "^0.1.3",
"hexo-generator-index": "^0.2.1",
"hexo-generator-tag": "^0.2.0",
"hexo-renderer-ejs": "^0.3.1",
"hexo-renderer-stylus": "^0.3.3",
"hexo-renderer-marked": "^1.0.1",
"hexo-server": "^0.3.3"
}
scaffolds:模板文件夹,是指在新建的文章文件中默认填充的内容。例如,如果您修改scaffold/post.md中的Front-matter内容,那么每次新建一篇文章时都会包含这个修改。
source:资源文件夹,存放用户资源的地方。除_posts
文件夹之外,开头命名为 _ (下划线)的文件/文件夹和隐藏的文件将会被忽略。Markdown 和 HTML 文件会被解析并放到 public 文件夹,而其他文件会被拷贝过去。
themes:主题文件夹。Hexo 会根据主题来生成静态页面。
node_modules:node.js 模块,一些 插件 和 依赖 会被安装到这里。
更加详细的解释请参考hexo 官方文档
安装 NexT 主题
进入本地博客文件夹并将 NexT 主题clone
至themes
文件夹下
1
git clone https://github.com/theme-next/hexo-theme-next themes/next
你会看到,在next
下也有一个_config.yml
的文件,这是 NexT 主题的配置文件,为了区别它和 博客配置文件,下面会用带路径的文件名来描述它们:
<folder>/_config.yml
:站点配置文件next/_config.yml
:主题配置文件
启用 NexT 主题
在<folder>/_config.yml
里theme:
选项填next
,=>theme: next
,注意冒号后空一格。
到这里,建站的任务就完成了。你现在可以打开控制台,输入并执行如下命令:
1
hexo g
完成没有报错之后执行如下命令:
1
hexo s
其中
hexo g
:新建public
文件夹,并在其中生成网站静态文件(html,css,等文件)hexo s
:启动 hexo 服务器,默认情况下,访问网址为:http://localhost:4000/
你最后会看到控制台有如下输出:
1
INFO Hexo is running at http://localhost:4000/. Press Ctrl+C to stop.
在浏览器地址栏输入http://localhost:4000/
并访问,你应该会看到如下页面:

**恭喜你!你已经完成了博客搭建的主要工作!接下来就是细节的配置了。请耐心阅读以下内容。**配置
网站脚注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
footer:
#建站时间
since: 2018
#作者头像并且是动画效果
icon:
name: user
animated: true
color: "##66CDAA"
#显示版权作者
copyright: aigisss 爱即是诗
#不显示Hexo
powered:
enable: false
version: false
#不显示主题和版本
theme:
enable: false
version: false
#显示备案号
beian:
enable: true
icp: 赣ICP备18013338-1号
版权声明
1
2
3
4
creative_commons:
license: by-nc-sa
sidebar: false
post: true
代码块
1
2
3
4
5
6
7
8
9
codeblock:
# 自定义边框半径,默认是1
# 值越大弧度越大
border_radius: 6
# 右上角显示复制按钮
copy_button:
enable: true
# 显示复制结果
show_result: true
分享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
needmoreshare2:
enable: true
postbottom:
#文章底部
enable: false
options:
iconStyle: box
boxForm: horizontal
position: bottomCenter
networks: Weibo,Wechat,Douban,QQZone,Twitter,Facebook
#左下角悬浮按钮
float:
enable: true
options:
iconStyle: box
boxForm: horizontal
position: middleRight
networks: Weibo,Wechat,Douban,QQZone,Twitter,Facebook
访问次数
1
2
3
4
5
6
7
8
9
10
11
12
# busuanzi统计
busuanzi_count:
enable: true
# 总访客数
total_visitors: true
total_visitors_icon: user
# 总浏览量
total_views: true
total_views_icon: eye
# 文章浏览量
post_views: true
post_views_icon: eye
顶部阅读进度条
1
2
3
4
reading_progress:
enable: true
color: "#37c6c0"
height: 2px
加载动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
motion:
# 启用
enable: true
# 异步加载
async: true
transition:
# Transition variants:
# fadeIn | fadeOut | flipXIn | flipXOut | flipYIn | flipYOut | flipBounceXIn | flipBounceXOut | flipBounceYIn | flipBounceYOut
# swoopIn | swoopOut | whirlIn | whirlOut | shrinkIn | shrinkOut | expandIn | expandOut
# bounceIn | bounceOut | bounceUpIn | bounceUpOut | bounceDownIn | bounceDownOut | bounceLeftIn | bounceLeftOut | bounceRightIn | bounceRightOut
# slideUpIn | slideUpOut | slideDownIn | slideDownOut | slideLeftIn | slideLeftOut | slideRightIn | slideRightOut
# slideUpBigIn | slideUpBigOut | slideDownBigIn | slideDownBigOut | slideLeftBigIn | slideLeftBigOut | slideRightBigIn | slideRightBigOut
# perspectiveUpIn | perspectiveUpOut | perspectiveDownIn | perspectiveDownOut | perspectiveLeftIn | perspectiveLeftOut | perspectiveRightIn | perspectiveRightOut
# 文章摘要动画
post_block: bounceIn
# 加载各种页面动画(分类,关于,标签等等)
post_header: fadeIn
# 文章详情动画
post_body: fadeIn
#
coll_header: fadeIn
# Only for Pisces | Gemini.
# 侧边栏(人物头像的那部分)
sidebar: fadeIn
搜索功能
NexT
自带提供了两个搜索
algolia_search
local_search
其实这个local_search
已经很好用了,配置algolia_search
挺麻烦的,而且搜索功能也用的不多
毕竟有万能的Ctrl + F
1
2
3
4
5
6
7
8
9
local_search:
enable: true
# if auto, trigger search by changing input
# if manual, trigger search by pressing enter key or search button
trigger: auto
# show top n results per article, show all results by setting to -1
top_n_per_article: 1
# unescape html strings to the readable one
unescape: false
添加 RSS 订阅
1
2
3
4
5
6
7
8
9
10
npm install hexo-generator-feed --save
复制代码
# Extensions
plugins:
hexo-generate-feed
feed: # RSS订阅插件
type: atom
path: atom.xml
limit: 0 #0就是代表所有
数学公式
1
2
3
4
5
6
7
8
9
10
11
# Math Equations Render Support
math:
enable: true
# Default(true) will load mathjax/katex script on demand
# That is it only render those page who has 'mathjax: true' in Front Matter.
# If you set it to false, it will load mathjax/katex srcipt EVERY PAGE.
per_page: true
engine: mathjax
#engine: katex
还需要在文章的 Front-matter 里打开 mathjax 开关,比如:
1
2
3
4
5
title: 使用hexo下next主题搭建博客的记录
tags: 日常
abbrlink: 326cb881
date: 2019-09-09 09:58:21
mathjax: true
网上一大堆说会出现语义冲突——-类 Latex 格式书写的数学公式下划线_
表示下标,有特殊的含义,如果被强制转换为<em>
标签,那么 MathJax 引擎在渲染数学公式的时候就会出错。类似的语义冲突的符号还包括*
, {
, }
, \\
等。但是!!
在我试验下没有出现此类问题,只要在主题中打开,md 中申明 mathjax: true 就好了,可能在我使用的next6.7
中解决了冲突。比如以下的公式能出来!
1
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
1
2
3
4
5
6
7
8
$$
P = \frac
{\sum_{i=1}^n (x_i- x)(y_i- y)}
{\displaystyle \left[
\sum_{i=1}^n (x_i-x)^2
\sum_{i=1}^n (y_i-y)^2
\right]^{1/2} }
$$
win系统下使用vscode中的remote-SSH插件连接就出错
本文最后更新于:9 个月前
有了自己的云服务器,就想把自己的网站往上迁移。听闻 vscode 中remote-SSH
的插件很好用,于是安装使用一下,木有想到的是,第一步就出错了——连接不上,报Bad owner or permissions
的错!!
网上试了很多种,方法,都没有解决,包括禁用继承、openssh-portable 。但是还是没有解决我的问题,但是在公司的电脑一下子就能连上。我就想着其中的区别,发现其中有点不同,结合上面的禁用继承的文章,于是把第一个用户名删了,我登录的是HelloWorld
这个用户。

删完的图如下:

结果能登录上去了,如图:

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+win系统下使用vscode中的remote-SSH插件连接就出错 - AIGISSS win系统下使用vscode中的remote-SSH插件连接就出错
本文最后更新于:9 个月前
有了自己的云服务器,就想把自己的网站往上迁移。听闻 vscode 中remote-SSH
的插件很好用,于是安装使用一下,木有想到的是,第一步就出错了——连接不上,报Bad owner or permissions
的错!!
网上试了很多种,方法,都没有解决,包括禁用继承、openssh-portable 。但是还是没有解决我的问题,但是在公司的电脑一下子就能连上。我就想着其中的区别,发现其中有点不同,结合上面的禁用继承的文章,于是把第一个用户名删了,我登录的是HelloWorld
这个用户。

删完的图如下:

结果能登录上去了,如图:

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/fdfea9f5.html b/posts/fdfea9f5.html
index 532e9e08..e7631a23 100644
--- a/posts/fdfea9f5.html
+++ b/posts/fdfea9f5.html
@@ -1 +1 @@
-三维模型资源获取 - AIGISSS 三维模型资源获取
本文最后更新于:6 个月前
前言
使用 mapbox-gl 加载三维模型,到处寻找也没有找到几个免费重要是能用的。模型分很多种,有 3ds、max、fbx、obj、mtl、blend、c4d、dae、gltf、glb 格式等等,之前一直使用 obj 和 mtl 的,但丢失材质有点严重。
寻找
机缘巧合之下,下载的包中含有 fbx 格式的,双击竟然能打开,原来是win10
自带的 3D 查看器,右上角有一个 3D 资源库,在里面搜了一下,发现好多能用模型,可以另存为 glb 格式。glTF 文件格式有 .gltf
和 .glb
两种。glTF 文件的 buffer 部分在 .gltf
里是用 Base64 编码保存的,有时候也会被抽出来保存为 .bin
文件,导致最后其实有两个文件。
特别提醒
搜索的时候要用英文,不然会查不到!!


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+三维模型资源获取 - AIGISSS 三维模型资源获取
本文最后更新于:6 个月前
前言
使用 mapbox-gl 加载三维模型,到处寻找也没有找到几个免费重要是能用的。模型分很多种,有 3ds、max、fbx、obj、mtl、blend、c4d、dae、gltf、glb 格式等等,之前一直使用 obj 和 mtl 的,但丢失材质有点严重。
寻找
机缘巧合之下,下载的包中含有 fbx 格式的,双击竟然能打开,原来是win10
自带的 3D 查看器,右上角有一个 3D 资源库,在里面搜了一下,发现好多能用模型,可以另存为 glb 格式。glTF 文件格式有 .gltf
和 .glb
两种。glTF 文件的 buffer 部分在 .gltf
里是用 Base64 编码保存的,有时候也会被抽出来保存为 .bin
文件,导致最后其实有两个文件。
特别提醒
搜索的时候要用英文,不然会查不到!!


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/posts/shaderglossary.html b/posts/shaderglossary.html
index b26ef841..7d53e7d1 100644
--- a/posts/shaderglossary.html
+++ b/posts/shaderglossary.html
@@ -1 +1 @@
-shader常用的词汇表 - AIGISSS shader常用的词汇表
本文最后更新于:1 个月前
类型
void
bool
int
float
bvec2
bvec3
bvec4
ivec2
ivec3
ivec4
mat2
mat3
mat4
4x4
浮点矩阵
声明
1
2
3
4
5
6
7
mat4 aMat4 = mat4(1.0, 0.0, 0.0, 0.0, // 1. column
0.0, 1.0, 0.0, 0.0, // 2. column
0.0, 0.0, 1.0, 0.0, // 3. column
0.0, 0.0, 0.0, 1.0); // 4. column
mat4 bMat4 = mat4(1.0);
mat4 cMat4 = mat4(aVec4, bVec4, cVec4, dVec4);
mat4 dMat4 = mat4(aVec4, aVec3, bVec4, cVec4, aFloat);
mat4
数据类型是由浮点4x4
矩阵组成的。如上所示,可以通过不同的方式初始化:
- 逐列为每个组件提供值。
- 提供一个用于主对角线上的组件的值。
- 提供向量和标量的组合。
以同样的方式,可以按组件方式或按列访问数据:
1
2
3
4
5
aMat4[3][3] = 1.0;
float aFloat = aMat4[3][3];
aMat4[0] = vec4(1.0);
vec4 aVec4 = aMat4[0];
smapler2D
smaplerCube
struct
结构变量类型,例子
1
2
3
4
5
6
7
8
9
10
11
struct matStruct {
vec4 ambientColor;
vec4 diffuseColor;
vec4 specularColor;
float specularExponent;
} newMaterial;
newMaterial = matStruct(vec4(0.1, 0.1, 0.1, 1.0),
vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.7, 0.7, 0.7, 1.0),
50.0);
描述
struct
声明基于标准类型的自定义数据结构。具有相同名称的结构的构造函数将自动创建。变量的声明(在本例中为newMaterial
)是可选的。
限定词
attribute
顶点属性数据。
例如:
1
attribute vec4 v_color;
attribute
只读变量,包含从WebGL / OpenGL环境共享到顶点着色器的数据。
由于顶点着色器对每个顶点执行一次,因此通常为每个顶点数据指定属性,并使用以下信息:顶点的空间位置,颜色,法线方向和纹理坐标。
const
常量限定词
例如:
1
const float PI = 3.14159265359;
const限定符可以应用于任何变量的声明,以指定其值不会更改。
uniform
统一变量限定符。
例子:
1
uniform vec4 direction;
uniform
变量包含从WebGL / OpenGL环境共享到顶点或片段着色器的只读数据。 该值是针对每个图元的,因此对于在图元,帧或场景中保持不变的变量很有用。
varying
可变变量限定符。
例子:
1
varying vec3 position;
varying
变量包含从顶点着色器到片段着色器共享的数据。 必须在顶点着色器中写入变量,然后从组成片段的顶点内插片段着色器中的只读值。
precision
highp
mediump
lowp
in
out
inout
内置变量
gl_Position
gl_PointSize
gl_PointCoord
gl_FrontFacing
gl_FragCoord
gl_FragColor
内置常数
gl_MaxVertexAttribs
gl_MaxVaryingVectors
gl_MaxVertexTextureImageUnits
gl_MaxCombinedTextureImageUnits
gl_MaxTextureImageUnits
gl_MaxFragmentUniformVectors
gl_MaxDrawBuffers
角度和三角函数
radians()
将数量转换为弧度
1
2
3
4
float radians(float degrees)
vec2 radians(vec2 degrees)
vec3 radians(vec3 degrees)
vec4 radians(vec4 degrees)
参数:degrees
指定要转换为弧度的数量(以度为单位)。
描述:radians()
将以度为单位的数量转换为弧度。即返回值为(PI * degrees)/180
。
degrees()
将弧度转换为度
1
2
3
4
float degrees(float radians)
vec2 degrees(vec2 radians)
vec3 degrees(vec3 radians)
vec4 degrees(vec4 radians)
参数:radians
指定要转换为度的数量(以弧度为单位)。
描述:degrees()
将以弧度表示的数量转换为度数。 也就是说,返回值为(180.0*radians)/PI
sin()
返回参数的正弦
1
2
3
4
float sin(float angle)
vec2 sin(vec2 angle)
vec3 sin(vec3 angle)
vec4 sin(vec4 angle)
参数:angle
指定以弧度表示的要返回正弦的量。
描述:sin()
返回角度的三角正弦值。
cos()
tan()
asin()
acos()
atan()
指数函数
pow()
将第一个参数的值返回第二个参数的幂。
1
2
3
4
float pow(float x, float y)
vec2 pow(vec2 x, vec2 y)
vec3 pow(vec3 x, vec3 y)
vec4 pow(vec4 x, vec4 y)
exp()
返回参数的自然幂
1
2
3
4
float exp(float x)
vec2 exp(vec2 x)
vec3 exp(vec3 x)
vec4 exp(vec4 x)
log()
exp2()
log2()
sqrt()
inversesqrt()
常用函数
abs()
sign()
floor()
ceil()
fract()
计算参数的小数部分
1
2
3
4
float fract(float x)
vec2 fract(vec2 x)
vec3 fract(vec3 x)
vec4 fract(vec4 x)
x
指定要评估的值。
fract()
返回x的小数部分。计算公式为x-floor(x)
。
mod()
计算一个参数取模的值
1
2
3
4
5
6
7
8
float mod(float x, float y)
vec2 mod(vec2 x, vec2 y)
vec3 mod(vec3 x, vec3 y)
vec4 mod(vec4 x, vec4 y)
vec2 mod(vec2 x, float y)
vec3 mod(vec3 x, float y)
vec4 mod(vec4 x, float y)
参数:x
指定要评估的值。 y
指定要获取其模数的值。
描述:mod()
返回x模y的值。计算为x-y * floor(x / y)
。
min()
max()
clamp()
mix()
step()
通过比较两个值生成阶跃函数
1
2
3
4
5
6
7
8
float step(float edge, float x)
vec2 step(vec2 edge, vec2 x)
vec3 step(vec3 edge, vec3 x)
vec4 step(vec4 edge, vec4 x)
vec2 step(float edge, vec2 x)
vec3 step(float edge, vec3 x)
vec4 step(float edge, vec4 x)
参数:
edge
指定步进函数的边缘位置。
x
指定用于生成步进函数的值。
描述:
step()
通过将x
与edge
进行比较来生成step函数。 对于返回值的元素i
,如果x[i] <edge[i]
返回0.0
,否则返回1.0
。
smoothstep()
在两个值之间执行Hermite插值
1
2
3
4
5
6
7
8
float smoothstep(float edge0, float edge1, float x)
vec2 smoothstep(vec2 edge0, vec2 edge1, vec2 x)
vec3 smoothstep(vec3 edge0, vec3 edge1, vec3 x)
vec4 smoothstep(vec4 edge0, vec4 edge1, vec4 x)
vec2 smoothstep(float edge0, float edge1, vec2 x)
vec3 smoothstep(float edge0, float edge1, vec3 x)
vec4 smoothstep(float edge0, float edge1, vec4 x)
参数:
edge0
指定Hermite函数下边缘的值。
edge1
指定Hermite函数的上边缘的值。
x
指定插值的源值。
描述:
当edge0 <x <edge1
时,smoothstep()
在0和1之间执行平滑的Hermite插值。这在需要具有平稳过渡的阈值函数的情况下很有用。 smoothstep()
等效于:
1
2
3
genType t; /* Or genDType t; */
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
如果edge0≥edge1
,则结果是undefined
。
几何函数
length()
distance()
dot()
计算两个向量的点积
1
2
3
4
float dot(float x, float y)
float dot(vec2 x, vec2 y)
float dot(vec3 x, vec3 y)
float dot(vec4 x, vec4 y)
参数:x
指定两个向量中的第一个 ,y
指定两个向量中的第二个
描述:dot()
返回两个向量x和y的点积。 即x[0]·y[0] + x[1]·y[1] + ...
如果x和y相同,则点积的平方根等于向量的长度。 输入参数可以是浮标量或浮标向量。 如果是浮标量,则点函数是微不足道的,并返回x和y的乘积。
cross()
计算两个向量的叉积
1
vec3 cross(vec3 x, vec3 y)
参数: x
指定两个向量中的第一个,y
指定两个向量中的第二个.
描述:
cross()
返回两个向量x和y的叉积。 输入参数只能是3分量浮点向量。 叉积等于向量长度乘以x和y之间(较小)角的正弦值的乘积。
normalize()
计算与输入向量相同方向的单位向量
1
2
3
4
float normalize(float x)
vec2 normalize(vec2 x)
vec3 normalize(vec3 x)
vec4 normalize(vec4 x)
x
指定要归一化的向量。
描述:normalize()
返回一个向量,向量的方向与其参数x相同,但长度为1。
facefoward()
返回指向与另一个方向相同的向量
1
2
3
4
float faceforward(float N, float I, float Nref)
vec2 faceforward(vec2 N, vec2 I, vec2 Nref)
vec3 faceforward(vec3 N, vec3 I, vec3 Nref)
vec4 faceforward(vec4 N, vec4 I, vec4 Nref)
reflect()
refract()
矩阵函数
matrixCompMult()
执行两个矩阵的按分量乘法
1
2
3
mat2 matrixCompMult(mat2 x, mat2 y)
mat3 matrixCompMult(mat3 x, mat3 y)
mat4 matrixCompMult(mat4 x, mat4 y)
参数:
x
指定第一个矩阵被乘数。
y
指定第二个矩阵被乘数。
描述:
matrixCompMult()
对两个矩阵进行按分量乘法,生成结果矩阵,其中每个分量result[i][j]
计算为x[i][j]
和y[i][j]
的标量积
向量函数
lessThan()
lessThanEqual()
greaterThan()
greaterThanEqual()
equal()
notEqual()
any()
all()
not()
纹理查找函数
texture2D()
textureCube()
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
+shader常用的词汇表 - AIGISSS shader常用的词汇表
本文最后更新于:1 分钟前
类型
void
bool
int
float
bvec2
bvec3
bvec4
ivec2
ivec3
ivec4
mat2
mat3
mat4
4x4
浮点矩阵
声明
1
2
3
4
5
6
7
mat4 aMat4 = mat4(1.0, 0.0, 0.0, 0.0, // 1. column
0.0, 1.0, 0.0, 0.0, // 2. column
0.0, 0.0, 1.0, 0.0, // 3. column
0.0, 0.0, 0.0, 1.0); // 4. column
mat4 bMat4 = mat4(1.0);
mat4 cMat4 = mat4(aVec4, bVec4, cVec4, dVec4);
mat4 dMat4 = mat4(aVec4, aVec3, bVec4, cVec4, aFloat);
mat4
数据类型是由浮点4x4
矩阵组成的。如上所示,可以通过不同的方式初始化:
- 逐列为每个组件提供值。
- 提供一个用于主对角线上的组件的值。
- 提供向量和标量的组合。
以同样的方式,可以按组件方式或按列访问数据:
1
2
3
4
5
aMat4[3][3] = 1.0;
float aFloat = aMat4[3][3];
aMat4[0] = vec4(1.0);
vec4 aVec4 = aMat4[0];
smapler2D
smaplerCube
struct
结构变量类型,例子
1
2
3
4
5
6
7
8
9
10
11
struct matStruct {
vec4 ambientColor;
vec4 diffuseColor;
vec4 specularColor;
float specularExponent;
} newMaterial;
newMaterial = matStruct(vec4(0.1, 0.1, 0.1, 1.0),
vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.7, 0.7, 0.7, 1.0),
50.0);
描述
struct
声明基于标准类型的自定义数据结构。具有相同名称的结构的构造函数将自动创建。变量的声明(在本例中为newMaterial
)是可选的。
限定词
attribute
顶点属性数据。
例如:
1
attribute vec4 v_color;
attribute
只读变量,包含从WebGL / OpenGL环境共享到顶点着色器的数据。
由于顶点着色器对每个顶点执行一次,因此通常为每个顶点数据指定属性,并使用以下信息:顶点的空间位置,颜色,法线方向和纹理坐标。
const
常量限定词
例如:
1
const float PI = 3.14159265359;
const限定符可以应用于任何变量的声明,以指定其值不会更改。
uniform
统一变量限定符。
例子:
1
uniform vec4 direction;
uniform
变量包含从WebGL / OpenGL环境共享到顶点或片段着色器的只读数据。 该值是针对每个图元的,因此对于在图元,帧或场景中保持不变的变量很有用。
varying
可变变量限定符。
例子:
1
varying vec3 position;
varying
变量包含从顶点着色器到片段着色器共享的数据。 必须在顶点着色器中写入变量,然后从组成片段的顶点内插片段着色器中的只读值。
precision
highp
mediump
lowp
in
out
inout
内置变量
gl_Position
gl_PointSize
gl_PointCoord
gl_FrontFacing
gl_FragCoord
gl_FragColor
内置常数
gl_MaxVertexAttribs
gl_MaxVaryingVectors
gl_MaxVertexTextureImageUnits
gl_MaxCombinedTextureImageUnits
gl_MaxTextureImageUnits
gl_MaxFragmentUniformVectors
gl_MaxDrawBuffers
角度和三角函数
radians()
将数量转换为弧度
1
2
3
4
float radians(float degrees)
vec2 radians(vec2 degrees)
vec3 radians(vec3 degrees)
vec4 radians(vec4 degrees)
参数:degrees
指定要转换为弧度的数量(以度为单位)。
描述:radians()
将以度为单位的数量转换为弧度。即返回值为(PI * degrees)/180
。
degrees()
将弧度转换为度
1
2
3
4
float degrees(float radians)
vec2 degrees(vec2 radians)
vec3 degrees(vec3 radians)
vec4 degrees(vec4 radians)
参数:radians
指定要转换为度的数量(以弧度为单位)。
描述:degrees()
将以弧度表示的数量转换为度数。 也就是说,返回值为(180.0*radians)/PI
sin()
返回参数的正弦
1
2
3
4
float sin(float angle)
vec2 sin(vec2 angle)
vec3 sin(vec3 angle)
vec4 sin(vec4 angle)
参数:angle
指定以弧度表示的要返回正弦的量。
描述:sin()
返回角度的三角正弦值。
cos()
tan()
asin()
acos()
atan()
指数函数
pow()
将第一个参数的值返回第二个参数的幂。
1
2
3
4
float pow(float x, float y)
vec2 pow(vec2 x, vec2 y)
vec3 pow(vec3 x, vec3 y)
vec4 pow(vec4 x, vec4 y)
exp()
返回参数的自然幂
1
2
3
4
float exp(float x)
vec2 exp(vec2 x)
vec3 exp(vec3 x)
vec4 exp(vec4 x)
log()
exp2()
log2()
sqrt()
inversesqrt()
常用函数
abs()
sign()
floor()
ceil()
fract()
计算参数的小数部分
1
2
3
4
float fract(float x)
vec2 fract(vec2 x)
vec3 fract(vec3 x)
vec4 fract(vec4 x)
x
指定要评估的值。
fract()
返回x的小数部分。计算公式为x-floor(x)
。
mod()
计算一个参数取模的值
1
2
3
4
5
6
7
8
float mod(float x, float y)
vec2 mod(vec2 x, vec2 y)
vec3 mod(vec3 x, vec3 y)
vec4 mod(vec4 x, vec4 y)
vec2 mod(vec2 x, float y)
vec3 mod(vec3 x, float y)
vec4 mod(vec4 x, float y)
参数:x
指定要评估的值。 y
指定要获取其模数的值。
描述:mod()
返回x模y的值。计算为x-y * floor(x / y)
。
min()
max()
clamp()
mix()
step()
通过比较两个值生成阶跃函数
1
2
3
4
5
6
7
8
float step(float edge, float x)
vec2 step(vec2 edge, vec2 x)
vec3 step(vec3 edge, vec3 x)
vec4 step(vec4 edge, vec4 x)
vec2 step(float edge, vec2 x)
vec3 step(float edge, vec3 x)
vec4 step(float edge, vec4 x)
参数:
edge
指定步进函数的边缘位置。
x
指定用于生成步进函数的值。
描述:
step()
通过将x
与edge
进行比较来生成step函数。 对于返回值的元素i
,如果x[i] <edge[i]
返回0.0
,否则返回1.0
。
smoothstep()
在两个值之间执行Hermite插值
1
2
3
4
5
6
7
8
float smoothstep(float edge0, float edge1, float x)
vec2 smoothstep(vec2 edge0, vec2 edge1, vec2 x)
vec3 smoothstep(vec3 edge0, vec3 edge1, vec3 x)
vec4 smoothstep(vec4 edge0, vec4 edge1, vec4 x)
vec2 smoothstep(float edge0, float edge1, vec2 x)
vec3 smoothstep(float edge0, float edge1, vec3 x)
vec4 smoothstep(float edge0, float edge1, vec4 x)
参数:
edge0
指定Hermite函数下边缘的值。
edge1
指定Hermite函数的上边缘的值。
x
指定插值的源值。
描述:
当edge0 <x <edge1
时,smoothstep()
在0和1之间执行平滑的Hermite插值。这在需要具有平稳过渡的阈值函数的情况下很有用。 smoothstep()
等效于:
1
2
3
genType t; /* Or genDType t; */
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
如果edge0≥edge1
,则结果是undefined
。
几何函数
length()
distance()
dot()
计算两个向量的点积
1
2
3
4
float dot(float x, float y)
float dot(vec2 x, vec2 y)
float dot(vec3 x, vec3 y)
float dot(vec4 x, vec4 y)
参数:x
指定两个向量中的第一个 ,y
指定两个向量中的第二个
描述:dot()
返回两个向量x和y的点积。 即x[0]·y[0] + x[1]·y[1] + ...
如果x和y相同,则点积的平方根等于向量的长度。 输入参数可以是浮标量或浮标向量。 如果是浮标量,则点函数是微不足道的,并返回x和y的乘积。
cross()
计算两个向量的叉积
1
vec3 cross(vec3 x, vec3 y)
参数: x
指定两个向量中的第一个,y
指定两个向量中的第二个.
描述:
cross()
返回两个向量x和y的叉积。 输入参数只能是3分量浮点向量。 叉积等于向量长度乘以x和y之间(较小)角的正弦值的乘积。
normalize()
计算与输入向量相同方向的单位向量
1
2
3
4
float normalize(float x)
vec2 normalize(vec2 x)
vec3 normalize(vec3 x)
vec4 normalize(vec4 x)
x
指定要归一化的向量。
描述:normalize()
返回一个向量,向量的方向与其参数x相同,但长度为1。
facefoward()
返回指向与另一个方向相同的向量
1
2
3
4
float faceforward(float N, float I, float Nref)
vec2 faceforward(vec2 N, vec2 I, vec2 Nref)
vec3 faceforward(vec3 N, vec3 I, vec3 Nref)
vec4 faceforward(vec4 N, vec4 I, vec4 Nref)
reflect()
refract()
矩阵函数
matrixCompMult()
执行两个矩阵的按分量乘法
1
2
3
mat2 matrixCompMult(mat2 x, mat2 y)
mat3 matrixCompMult(mat3 x, mat3 y)
mat4 matrixCompMult(mat4 x, mat4 y)
参数:
x
指定第一个矩阵被乘数。
y
指定第二个矩阵被乘数。
描述:
matrixCompMult()
对两个矩阵进行按分量乘法,生成结果矩阵,其中每个分量result[i][j]
计算为x[i][j]
和y[i][j]
的标量积
向量函数
lessThan()
lessThanEqual()
greaterThan()
greaterThanEqual()
equal()
notEqual()
any()
all()
not()
纹理查找函数
texture2D()
textureCube()
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!
目录
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
index 4a0347d2..5fed9184 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,63 +2,119 @@
- https://www.aigisss.com/blog/photos/index.html
+ https://www.aigisss.com/blog/posts/cc12ec28.html
- 2021-01-09
+ 2021-01-10
- https://www.aigisss.com/blog/photos/photos.json
+ https://www.aigisss.com/blog/posts/2205c935.html
- 2021-01-09
+ 2021-01-10
- https://www.aigisss.com/blog/about/index.html
+ https://www.aigisss.com/blog/posts/shaderglossary.html
- 2021-01-09
+ 2021-01-10
- https://www.aigisss.com/blog/posts/d673e1b7.html
+ https://www.aigisss.com/blog/posts/cb3db6e0.html
- 2020-12-31
+ 2021-01-10
- https://www.aigisss.com/blog/posts/2205c935.html
+ https://www.aigisss.com/blog/posts/a2d9d205.html
+
+ 2021-01-10
+
+
+
+
+ https://www.aigisss.com/blog/posts/5796179d.html
- 2020-12-30
+ 2021-01-10
+
+
+
+
+ https://www.aigisss.com/blog/posts/5796179d.html
+
+ 2021-01-10
+
+
+
+
+ https://www.aigisss.com/blog/posts/9604e2a7.html
+
+ 2021-01-10
+
+
+
+
+ https://www.aigisss.com/blog/posts/9604abcd.html
+
+ 2021-01-10
+
+
+
+
+ https://www.aigisss.com/blog/posts/e6c6b2b1.html
+
+ 2021-01-10
https://www.aigisss.com/blog/posts/5a12ed89.html
- 2020-12-04
+ 2021-01-10
- https://www.aigisss.com/blog/posts/4ce65006.html
+ https://www.aigisss.com/blog/posts/d673e1b7.html
- 2020-12-01
+ 2021-01-10
- https://www.aigisss.com/blog/posts/c22fb8cf.html
+ https://www.aigisss.com/blog/photos/index.html
+
+ 2021-01-09
+
+
+
+
+ https://www.aigisss.com/blog/photos/photos.json
+
+ 2021-01-09
+
+
+
+
+ https://www.aigisss.com/blog/about/index.html
+
+ 2021-01-09
+
+
+
+
+ https://www.aigisss.com/blog/posts/4ce65006.html
2020-12-01
- https://www.aigisss.com/blog/posts/shaderglossary.html
+ https://www.aigisss.com/blog/posts/c22fb8cf.html
2020-12-01
@@ -197,13 +253,6 @@
-
- https://www.aigisss.com/blog/posts/9604abcd.html
-
- 2020-07-02
-
-
-
https://www.aigisss.com/blog/posts/9604e2c1.html
@@ -302,20 +351,6 @@
-
- https://www.aigisss.com/blog/posts/cc12ec28.html
-
- 2020-06-19
-
-
-
-
- https://www.aigisss.com/blog/posts/cb3db6e0.html
-
- 2020-06-05
-
-
-
https://www.aigisss.com/blog/posts/9c1bf78f.html
@@ -330,27 +365,6 @@
-
- https://www.aigisss.com/blog/posts/5796179d.html
-
- 2020-05-16
-
-
-
-
- https://www.aigisss.com/blog/posts/a2d9d205.html
-
- 2020-05-16
-
-
-
-
- https://www.aigisss.com/blog/posts/5796179d.html
-
- 2020-05-16
-
-
-
https://www.aigisss.com/blog/posts/fa7b679e.html
@@ -365,13 +379,6 @@
-
- https://www.aigisss.com/blog/posts/9604e2a7.html
-
- 2019-12-27
-
-
-
https://www.aigisss.com/blog/posts/3d0c447.html
@@ -393,13 +400,6 @@
-
- https://www.aigisss.com/blog/posts/e6c6b2b1.html
-
- 2019-10-21
-
-
-
https://www.aigisss.com/blog/posts/ab44f639.html
@@ -473,7 +473,7 @@
https://www.aigisss.com/blog
- 2021-01-09
+ 2021-01-10
daily
1.0
@@ -481,189 +481,189 @@
https://www.aigisss.com/blog/tags/%E6%80%BB%E7%BB%93/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/JavaScript/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/javascript/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/mapboxgl/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/Threejs/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/python/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/vscode/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E9%83%A8%E7%BD%B2/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E7%A4%BA%E4%BE%8B/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/Hexo/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/postgres/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/postgis/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E7%94%A8%E6%88%B7%E7%BB%8F%E9%AA%8C/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E8%8A%B1%E9%87%8C%E8%83%A1%E5%93%A8/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/Fluid/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/gitlab/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/git/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/mapbox/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/Django/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/NexT/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/leetcode/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E5%BB%BA%E7%AB%99/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E7%9D%80%E8%89%B2%E5%99%A8%E4%B9%8B%E4%B9%A6/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/nginx/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/%E6%91%98%E6%8A%84/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/vue/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/tags/webpack/
- 2021-01-09
+ 2021-01-10
daily
0.6
@@ -672,70 +672,70 @@
https://www.aigisss.com/blog/categories/%E6%8A%80%E6%9C%AF/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E5%AE%9E%E7%94%A8%E6%8A%80%E5%B7%A7/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E5%8A%9F%E8%83%BD%E5%A2%9E%E5%BC%BA/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E4%B8%BB%E9%A2%98%E7%A4%BA%E4%BE%8B/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E7%BC%96%E7%A8%8B/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E7%BC%96%E7%A8%8B/%E6%9B%B4%E6%96%B0%E4%B8%AD/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/mapbox/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E7%9D%80%E8%89%B2%E5%99%A8/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E6%9C%AA%E5%88%86%E7%B1%BB/
- 2021-01-09
+ 2021-01-10
daily
0.6
https://www.aigisss.com/blog/categories/%E6%8A%80%E6%9C%AF/%E6%9B%B4%E6%96%B0%E4%B8%AD/
- 2021-01-09
+ 2021-01-10
daily
0.6
diff --git a/tags/Django/index.html b/tags/Django/index.html
index 0fefad3e..57433498 100644
--- a/tags/Django/index.html
+++ b/tags/Django/index.html
@@ -1 +1 @@
-标签 - Django - AIGISSS
\ No newline at end of file
+标签 - Django - AIGISSS
\ No newline at end of file
diff --git a/tags/Fluid/index.html b/tags/Fluid/index.html
index dec5d4fd..e633bc69 100644
--- a/tags/Fluid/index.html
+++ b/tags/Fluid/index.html
@@ -1 +1 @@
-标签 - Fluid - AIGISSS
\ No newline at end of file
+标签 - Fluid - AIGISSS
\ No newline at end of file
diff --git a/tags/Hexo/index.html b/tags/Hexo/index.html
index 829782bf..ce52f12e 100644
--- a/tags/Hexo/index.html
+++ b/tags/Hexo/index.html
@@ -1 +1 @@
-标签 - Hexo - AIGISSS
\ No newline at end of file
+标签 - Hexo - AIGISSS
\ No newline at end of file
diff --git a/tags/Hexo/page/2/index.html b/tags/Hexo/page/2/index.html
index 8c509916..531ccf34 100644
--- a/tags/Hexo/page/2/index.html
+++ b/tags/Hexo/page/2/index.html
@@ -1 +1 @@
-标签 - Hexo - AIGISSS
\ No newline at end of file
+标签 - Hexo - AIGISSS
\ No newline at end of file
diff --git a/tags/NexT/index.html b/tags/NexT/index.html
index 79639d7a..ad4c555e 100644
--- a/tags/NexT/index.html
+++ b/tags/NexT/index.html
@@ -1 +1 @@
-标签 - NexT - AIGISSS
\ No newline at end of file
+标签 - NexT - AIGISSS
\ No newline at end of file
diff --git a/tags/Threejs/index.html b/tags/Threejs/index.html
index 4c43c36f..bc4359c5 100644
--- a/tags/Threejs/index.html
+++ b/tags/Threejs/index.html
@@ -1 +1 @@
-标签 - Threejs - AIGISSS
\ No newline at end of file
+标签 - Threejs - AIGISSS
\ No newline at end of file
diff --git a/tags/git/index.html b/tags/git/index.html
index 517a75eb..fe7320b9 100644
--- a/tags/git/index.html
+++ b/tags/git/index.html
@@ -1 +1 @@
-标签 - git - AIGISSS
\ No newline at end of file
+标签 - git - AIGISSS
\ No newline at end of file
diff --git a/tags/gitlab/index.html b/tags/gitlab/index.html
index 7445a44e..bd52479c 100644
--- a/tags/gitlab/index.html
+++ b/tags/gitlab/index.html
@@ -1 +1 @@
-标签 - gitlab - AIGISSS
\ No newline at end of file
+标签 - gitlab - AIGISSS
\ No newline at end of file
diff --git a/tags/index.html b/tags/index.html
index 6059a0af..c1a94f52 100644
--- a/tags/index.html
+++ b/tags/index.html
@@ -1 +1 @@
-标签 - AIGISSS
\ No newline at end of file
+标签 - AIGISSS
\ No newline at end of file
diff --git a/tags/javascript/index.html b/tags/javascript/index.html
index 8e0605ec..e964a494 100644
--- a/tags/javascript/index.html
+++ b/tags/javascript/index.html
@@ -1 +1 @@
-标签 - javascript - AIGISSS
\ No newline at end of file
+标签 - javascript - AIGISSS
\ No newline at end of file
diff --git a/tags/leetcode/index.html b/tags/leetcode/index.html
index a9ff9217..1d08bac5 100644
--- a/tags/leetcode/index.html
+++ b/tags/leetcode/index.html
@@ -1 +1 @@
-标签 - leetcode - AIGISSS
\ No newline at end of file
+标签 - leetcode - AIGISSS
\ No newline at end of file
diff --git a/tags/mapbox/index.html b/tags/mapbox/index.html
index 9d4be3f4..b884e591 100644
--- a/tags/mapbox/index.html
+++ b/tags/mapbox/index.html
@@ -1 +1 @@
-标签 - mapbox - AIGISSS
\ No newline at end of file
+标签 - mapbox - AIGISSS
\ No newline at end of file
diff --git a/tags/mapboxgl/index.html b/tags/mapboxgl/index.html
index 5ee20396..c62747e5 100644
--- a/tags/mapboxgl/index.html
+++ b/tags/mapboxgl/index.html
@@ -1 +1 @@
-标签 - mapboxgl - AIGISSS
\ No newline at end of file
+标签 - mapboxgl - AIGISSS
\ No newline at end of file
diff --git a/tags/nginx/index.html b/tags/nginx/index.html
index e56851a9..850ada56 100644
--- a/tags/nginx/index.html
+++ b/tags/nginx/index.html
@@ -1 +1 @@
-标签 - nginx - AIGISSS
\ No newline at end of file
+标签 - nginx - AIGISSS
\ No newline at end of file
diff --git a/tags/postgis/index.html b/tags/postgis/index.html
index fbc82255..24e829a1 100644
--- a/tags/postgis/index.html
+++ b/tags/postgis/index.html
@@ -1 +1 @@
-标签 - postgis - AIGISSS
\ No newline at end of file
+标签 - postgis - AIGISSS
\ No newline at end of file
diff --git a/tags/postgres/index.html b/tags/postgres/index.html
index 055a44d1..8bf5fb14 100644
--- a/tags/postgres/index.html
+++ b/tags/postgres/index.html
@@ -1 +1 @@
-标签 - postgres - AIGISSS
\ No newline at end of file
+标签 - postgres - AIGISSS
\ No newline at end of file
diff --git a/tags/python/index.html b/tags/python/index.html
index 81ced467..e19a7b47 100644
--- a/tags/python/index.html
+++ b/tags/python/index.html
@@ -1 +1 @@
-标签 - python - AIGISSS
\ No newline at end of file
+标签 - python - AIGISSS
\ No newline at end of file
diff --git a/tags/vscode/index.html b/tags/vscode/index.html
index 0db3606e..e3349cb7 100644
--- a/tags/vscode/index.html
+++ b/tags/vscode/index.html
@@ -1 +1 @@
-标签 - vscode - AIGISSS
\ No newline at end of file
+标签 - vscode - AIGISSS
\ No newline at end of file
diff --git a/tags/vue/index.html b/tags/vue/index.html
index 97ff1119..8d085845 100644
--- a/tags/vue/index.html
+++ b/tags/vue/index.html
@@ -1 +1 @@
-标签 - vue - AIGISSS
\ No newline at end of file
+标签 - vue - AIGISSS
\ No newline at end of file
diff --git a/tags/webpack/index.html b/tags/webpack/index.html
index 1d544631..277c71ef 100644
--- a/tags/webpack/index.html
+++ b/tags/webpack/index.html
@@ -1 +1 @@
-标签 - webpack - AIGISSS
\ No newline at end of file
+标签 - webpack - AIGISSS
\ No newline at end of file
diff --git "a/tags/\345\273\272\347\253\231/index.html" "b/tags/\345\273\272\347\253\231/index.html"
index 75547b80..7bcde8f9 100644
--- "a/tags/\345\273\272\347\253\231/index.html"
+++ "b/tags/\345\273\272\347\253\231/index.html"
@@ -1 +1 @@
-标签 - 建站 - AIGISSS
\ No newline at end of file
+标签 - 建站 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\346\200\273\347\273\223/index.html" "b/tags/\346\200\273\347\273\223/index.html"
index f3a08d00..9a525aa1 100644
--- "a/tags/\346\200\273\347\273\223/index.html"
+++ "b/tags/\346\200\273\347\273\223/index.html"
@@ -1 +1 @@
-标签 - 总结 - AIGISSS
\ No newline at end of file
+标签 - 总结 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\346\221\230\346\212\204/index.html" "b/tags/\346\221\230\346\212\204/index.html"
index 94d65f75..be7d389c 100644
--- "a/tags/\346\221\230\346\212\204/index.html"
+++ "b/tags/\346\221\230\346\212\204/index.html"
@@ -1 +1 @@
-标签 - 摘抄 - AIGISSS
\ No newline at end of file
+标签 - 摘抄 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\347\224\250\346\210\267\347\273\217\351\252\214/index.html" "b/tags/\347\224\250\346\210\267\347\273\217\351\252\214/index.html"
index 00383c88..041b67b6 100644
--- "a/tags/\347\224\250\346\210\267\347\273\217\351\252\214/index.html"
+++ "b/tags/\347\224\250\346\210\267\347\273\217\351\252\214/index.html"
@@ -1 +1 @@
-标签 - 用户经验 - AIGISSS
\ No newline at end of file
+标签 - 用户经验 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\347\235\200\350\211\262\345\231\250\344\271\213\344\271\246/index.html" "b/tags/\347\235\200\350\211\262\345\231\250\344\271\213\344\271\246/index.html"
index bfeacd2a..8250c8cb 100644
--- "a/tags/\347\235\200\350\211\262\345\231\250\344\271\213\344\271\246/index.html"
+++ "b/tags/\347\235\200\350\211\262\345\231\250\344\271\213\344\271\246/index.html"
@@ -1 +1 @@
-标签 - 着色器之书 - AIGISSS
\ No newline at end of file
+标签 - 着色器之书 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\347\244\272\344\276\213/index.html" "b/tags/\347\244\272\344\276\213/index.html"
index bc5a76d6..eebda320 100644
--- "a/tags/\347\244\272\344\276\213/index.html"
+++ "b/tags/\347\244\272\344\276\213/index.html"
@@ -1 +1 @@
-标签 - 示例 - AIGISSS
\ No newline at end of file
+标签 - 示例 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\350\212\261\351\207\214\350\203\241\345\223\250/index.html" "b/tags/\350\212\261\351\207\214\350\203\241\345\223\250/index.html"
index ccfb9606..a3b99ee2 100644
--- "a/tags/\350\212\261\351\207\214\350\203\241\345\223\250/index.html"
+++ "b/tags/\350\212\261\351\207\214\350\203\241\345\223\250/index.html"
@@ -1 +1 @@
-标签 - 花里胡哨 - AIGISSS
\ No newline at end of file
+标签 - 花里胡哨 - AIGISSS
\ No newline at end of file
diff --git "a/tags/\351\203\250\347\275\262/index.html" "b/tags/\351\203\250\347\275\262/index.html"
index da936f2b..529a9178 100644
--- "a/tags/\351\203\250\347\275\262/index.html"
+++ "b/tags/\351\203\250\347\275\262/index.html"
@@ -1 +1 @@
-标签 - 部署 - AIGISSS
\ No newline at end of file
+标签 - 部署 - AIGISSS
\ No newline at end of file