专栏名称: OSC开源社区
OSChina 开源中国 官方微信账号
目录
相关文章推荐
程序员的那些事  ·  趣图:高级 HTML 开发者 ·  昨天  
OSC开源社区  ·  自如基于StreamPark+Paimon实 ... ·  5 天前  
程序猿  ·  这不是找开发人员,这是找超人 ·  1 周前  
51好读  ›  专栏  ›  OSC开源社区

如何用 JS 在 Canvas 2D 上做出 3D 效果

OSC开源社区  · 公众号  · 程序员  · 2017-06-03 08:39

正文


逛 TypeScript 官网时发现了一个 js canvas 做的 3D 灯光渲染效果(详见参考1),一看源代码,竟然是用 getContext("2d") 做的,而且代码并不长,才两百多行,当时就被震惊了!!!

区区这点代码就能白手起家写出三维光线追踪渲染效果?!WTF!还有这种操作?!感觉这一定是涉及非常基础、核心的 3D 相关原理的代码。虽然之前用过很多 3D 软件,也学过一点 Direct3D 和 WebGL,但是对 3D 基础知识始终是一知半解的,向量、矩阵变换等计算也不是太清楚,于是立即决定深入研究一下。

折腾了一阵原代码,发现他的坐标空间是这样的,X正朝右,Y正朝上,Z正深入画布:

    

让我恍然大悟的是透视的处理方式,并不是每个物体的每个部分去计算离相机有多远,而是从相机中心点发射出许多射线(每像素一个射线),射线撞到的最近的物体就取这个物体的颜色,这样自然就能产生近大远小的效果。为了直观地理解摄像机的工作原理,我用 MaxScript 在 3DsMax 里建立了射线的模型,下面会说创建过程,先看结果:

    

这里设置了画布大小为 10 * 10 像素,每个像素对应一根射线,起点就是相机所在位置。

来看下近大远小的透视是如何形成的。为了清晰这里用了 5 * 5 像素的画布,同样在 3DsMax 里建立射线模型,并添加一个矩形平面,从顶视图中可以看见,矩形靠近摄像机的边交叉了 5 根射线,而远离摄像机的边只交叉了 3 根射线,每根射线对应一个像素,那么靠近摄像机的边在画布上会占了 5 个像素,远离摄像机的边则只占 3 个像素,近大远小的透视效果就这样自然而然地出来了。

理解了摄像机的射线,灯光渲染也就容易了,无非就是在这基础上,增加了法线、反射、RGB颜色混合、距离衰减等计算,这些其实都是基于向量的。

由于射线数量太多,手动创建太麻烦,幸好 3DsMax 提供了 MaxScript 这个脚本语言,我就用 js 拼接出 MaxScript 来使用。这个是 js 方法

源码是在 getPoint 方法里生成摄像机射线的,然后就在 getPoint 调用结果后面,把生成的射线存到 MaxScript 里,最后 log 出来。

控制台里输出的结果是这样的

打开 3DsMax 按 F11 调出 MaxScript 编辑器,把 js 结果贴入并全选,然后按 shift+enter 执行。

关闭 MaxScript 编辑器,按 Z 自动缩放一下,就可以看到射线已经生成了。  注意 3DsMax 的坐标系统和源代码不一样,3DsMax 里是这样的,X正同样朝右,但Y正深入画布,Z正朝上。其实和源码的坐标系统也很好转换,只要交换YZ即可。

    

到此对原码已经有了个系统的认识了,但是必须得自己敲点代码、算点公式才能称得上真正掌握。源码使用了球形和平面来演示,但是一般3D游戏等不会直接计算球形、平面,而是所有的东西都是用三角型拼出来的。那目标就很明确了,就是自己尝试做一个三角面。

第一步是复制源码的 Plane 类,改名为 Triangle,然后就是写 intersect 方法,一开始想自己做射线与三角面交叉的计算,初步思路是先计算射线与三角面所在平面的交叉点,然后把这点转换成平面所在坐标系,再判断这点是否落在三角形内,但因为涉及坐标系转换等计算太复杂,最后没有自己写,而是在网上找到了一个现成的、优化的非常好的代码(详见参考2)。里面也提到了一种老式算法没他好,这个老式算法基本和我一开始的思路一致。他给出了新算法的 C 代码,我转了一下 js,结果如下

注意这里只包含单面的交叉检测,即只有正面是看得见的(会和射线碰撞),背面是透明的(射线会穿透)。正反面的判断是这样的,三个点如此排列,上面为正面

反之用 1 3 2 的顺序创建 Triangle 的话,下面为正面。

在源码 draw 方法的 things 数组里加入我们的 Triangle, 调整摄像机的位置和注视点参数,让他能看见三角形正面(同样可以先在 3DsMax 里摆好位置,然后取各点的位置参数放到源码里,注意YZ要互换),就可以看到结果了,透视效果 OK 符合预期

 

至此静态模型的创建就算基本OK了,有了三角面,我们就可以拼出任意形状的模型。颜色的话前面分析过也并不是太难。当然深入的话还有反锯齿、光滑组、法线贴图、折射、粒子等等,那太高深了,留待后续研究。

现在不妨先来点动态的玩玩。源码只给了静态的,那动态的要怎么做呢?答案是矩阵变换。参考3里给出了关于矩阵非常好的例子,甚至 js 方法都给了现成的了。只要知道用矩阵从右到左的去叉乘点就可以让一个点的位置进行变换就行了。比如  X轴移动矩阵 × Y轴旋转矩阵 × X轴旋转矩阵 × 点 ,就是把这个点先根据X轴旋转,再根据Y轴旋转,最后在X轴上移动。可以把三角形的三个点同时都转一下,也可以只转摄像机的位置点。配合鼠标事件,我们的动态旋转效果就出来了。

  

在线演示 :   http://gonnavis.com/3d_raytracer

gitoschina源码: http://git.oschina.net/gonnavis/create3dfrom2d/tree/master

当然用 canvas2d 做 3d 效果只是纯粹为了研究原理,没有显卡加速,画布稍微大一点就卡得几乎不能动了。实际要做的话,肯定还是要用 canvas3d WebGL 的,或者直接用更高级的 BabylonJS ThreeJS 等库。

http://gonnavis.com/3d/babylonjs/rotate_obj_fallout_monster_2.html


参考

  1. https://www.typescriptlang.org/play/index.html 左侧下拉选择 Building a Raytracer 并点击右侧 Run.

  2. http://www.cs.virginia.edu/~gfx/Courses/2003/ImageSynthesis/papers/Acceleration/Fast%20MinimumStorage%20RayTriangle%20Intersection.pdf

  3. https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web



推荐阅读

6 月全球数据库排名,MySQL 直逼 Oracle | DB-Engines 排行榜

GO :互联网时代的 C 语言!| 码云周刊

Spring 思维导图,让 Spring 不再难懂(ioc 篇)

一名 40 岁“老”程序员的反思

“放码过来”邀您亮“项”,一不小心就火了!

点击“阅读原文”查看更多精彩内容