2015年1月28日星期三

Canvas之蛋疼的正方体绘制体验 - 子迟

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
Canvas之蛋疼的正方体绘制体验 - 子迟  阅读原文»

事情的起因

  之前写了篇谈谈文字图片粒子化 I,并且写了个简单的demo -> 粒子化。正当我在为写谈谈文字图片粒子化II准备demo时,突然想到能不能用正方体代替demo中的球体粒子。我不禁被自己的想法吓了一跳,球体的实现仅仅是简单的画圆,因为球体在任意角度任意距离的视图都是圆(如果有视图的话);而正方体有6个面8个点12条线,在canvas上的渲染多了n个数量级。先不说性能的问题,单单要实现六个面的旋转和绘制就不是一件特别容易的事情。

  说干就干,经过曲折的过程,终于得到了一个半成品 -> 粒子化之正方体

  

事情的经过

  事情的经过绝不像得到的结果那样简单。虽然半成品demo视觉上还有些许违和感,但已经能基本上达到我对粒子化特效的要求。

  那么接下来说说我这次的蛋疼体验吧。

  之前我们已经实现了一个点在三维系的坐标转换(如不懂,可参考 rotate 3d I),并且得到了这样的一个demo -> 3d球体。 那么我想,既然能得到点在三维系的空间转换坐标,根据点-线-面的原理,理论上应该很容易实现正方体在三维系的体现,不就是初始化相对位置一定的8个点么?而且之前也简单地实现了一个面的demo -> 3d爱心,当时认为并不难。

  于是我根据一定的相对位置,在三维系中初始化了8个点,每帧渲染的同时实现8个点的位置转移,并且根据8个点的位置每帧重绘12条线,得到demo -> 3d正方体

  似乎很顺利,接着给6个面上色,效果图如下:

  这时我意识到应该是面的绘制顺序出错了,在每帧的绘制前应该先给面排个序,比如图示的正方体的体心是三维系的原点,那么正方体的后面肯定是不可见的,所以应该先绘制。而在制作三维球体旋转时,是根据球体中心在三维系的坐标z值排序的,这一点也很好理解,越远的越容易被挡就越先画嘛;同时我在WAxes的这篇用Canvas玩3D:点-线-面中看到他绘制正方体的方法是根据6个面中心点的z值进行排序,乍一想似乎理所当然,于是我去实现了,体心在原点体验良好,demo -> 3d正方体,但是体心一改变位置,就坑爹了...

  

  图示的正方体体心在原点的右侧(沿x轴正方向),但是画出来的正方体却有违和感,为何?接着我还原了绘制的过程:

  

  绘制过程先绘制了正方体的左面,再绘制了上面,而根据生活经验这两个面的绘制顺序应该是先上面,再左面!不断的寻找错误,我发现这两个面中点的z值是一样的,甚至除了前后两个面,其他的四个面的z值都是一样的,也就是说这个例子中后面最先绘,前面最后绘,其他四个面的绘制顺序是任意的。我继续朝着这个方向前进,根据我的生活经验,如果像上图一样体心在原点右边(其实应该是视点,当时认为是原点),那么如果面的z值相同,应该根据面与原点的x方向的距离进行排序,毕竟距离小的先看到,如果x方向距离又相同,那么根据y方向的距离进行排序,代码如下:

  

var that = this;
this.f.sort(function (a, b) {
if(b.zIndex !== a.zIndex)
return b.zIndex - a.zIndex;
else if(b.xIndex !== a.xIndex) {
// 观察基准点(0,0,0)
if(that.x >= 0)
return b.xIndex - a.xIndex;
else
return a.xIndex - b.xIndex;
} else {
if(that.y >= 0)
return b.yIndex - a.yIndex;
else
return a.yIndex - b.yIndex;
}

  因为排序中this指向了window,还需赋值给一个另外的变量。事情似乎在此能画上一个圆满的句号,but...

  调整后下继续出现违和感(截图如下),虽然违和感的体验就在那么一瞬,但是我还是觉得是不是这个排序思路出错了?于是进一步验证,通过调试,将面的排序结果和正确的绘制顺序作对比,最终发现排序算法是错误的,最后知道真相的我眼泪掉下来。

  于是在知乎上问了下:怎样在二维上确定一个三维空间正方体六个面的绘制顺序? 有计算机图形学基础的请无视。

  原来这是一个古老的问题,在各位图形学大大的眼里是很基础的问题了。原来这个问题称为隐藏表面消除问题。

  然后我跟着这个方法进行了绘制,一开始把视点和原点搞混掉了。也就是判断每个面的法向量(不取指向体心的那条)和面(近似取面中心)到视点的那条向量之间的角度,如果小于90度则是可见。想了一下,似乎还真是那么一回事。然后需要设定视点的坐标,随意设置,只要合乎常理就行,这里我设置了(0,0,-500),在z方向肯定是个负值。

  一个正方体差不多搞定了,多个正方体呢?问题又出现:

  很显然,正方体之间也有绘制的先后顺序,这里粗略地采用根据体心排序的方法,按照Milo Yip的说法,这可以解决大部分情况,但也会漏掉一些最坏情况。最好的做法是zbuffer算法。

  于是乎,一个多正方体demo新鲜出炉了-> 多正方体demo

  如果要打造 粒子化之正方体 的效果,参考-> 谈谈文字图片粒子化 I

  梳理一下多正方体具体渲染过程:

  • 先将正方体进行排序,确定正方体的绘制顺序
  • 接着渲染每个正方体,先渲染正方体的各个点,改变各个点最新的坐标
for(var i = 0; i < 8; i++)
this.p.render();
  • 点渲染完后,根据最新的点的坐标调整正方体体心坐标,为下一帧的正方体排序准备
this.changeCoordinate();
  • 获取每个面法向量和面中点和视点夹角cos值,如果大于0(夹角小于90)则绘制:
for(var i = 0; i < 6; i++)
this.f.angle = this.f.getAngle();

this.f.sort(function (a, b) {
return a.angle > b.angle;
});

for(var i = 0; i < 6; i++) {
// 夹角 < 90,绘制
if(this.f.angle > 0)
this.f.draw();
}
  • 反复渲染

  完整代码如下:

1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5 <title> rotate 3d</title>
6 <script>
7 window.onload = function() {
8 var canvas = document.getElementById('canvas');
9 ES6笔记之参数默认值(译) - Bosn Ma  阅读原文»

  • 原文链接:http://dmitrysoshnikov.com/
  • 原文作者:Dmitry Soshnikov
  • 译者做了少量补充。这样的的文字是译者加的,可以选择忽略。

在这个简短的笔记中我们聊一聊ES6的又一特性:带默认值的函数参数。正如我们即将看到的,有些较为微妙的CASE。

ES5及以下手动处理默认值

在ES6默认值特性出现前,手动处理默认值有几种方式:

function log(message, level) {
level = level || 'warning';
console.log(level, ': ', message);
}

log('low memory'); // warning: low memory
log('out of memory', 'error'); // error: out of memory

为了处理参数未传递的情况,我们常看到typeof检测:

if (typeof level == 'undefined') {
level
= 'warning';
}

有时也可以检查arguments.length

if (arguments.length == 1) {
level
= 'warning';
}

这些方法都可以很好的工作,但都过于手动且缺少抽象。ES6规范了直接在函数头定义参数默认值的句法结构。

ES6默认值:基本例子

默认参数特性在很多语言中普遍存在,其基本形式可能大多数开发者都比较熟悉:

function log(message, level = 'warning') {
console.log(level,
': ', message);
}

log(
'low memory'); // warning: low memory
log('out of memory', 'error'); // error: out of memory

参数默认值使用方便且毫无违和感。接下来让我们深入细节实现,扫除默认参数所带来的一些困惑。

实现细节

以下为一些函数默认参数的ES6实现细节。

执行时求值

相对其它一些语言(如Python)在定义时一次性对默认值求值,ECMAScript在每次函数调用的执行期才会计算默认值。这种设计是为了避免在复杂对象作为默认值使用时引发一些困惑。接下来请看下面Python的例子:

def foo(x = []):
x.append(
1)
return x

# 我们可以看到默认值在函数定义时只创建了一次
#
并且存于函数对象的属性中
print(foo.__defaults__) # ([],)

foo()
# [1]
foo() # [1, 1]
foo() # [1, 1, 1]

print(foo.__defaults__) # ([1, 1, 1],)

为了避免这种现象,Python开发者通常把默认值定义为None,然后为这个值做显式检查:

def foo(x = None):
if x is None:
x
= []
x.append(
1)
print(x)

print(foo.__defaults__) # (None,)

foo()
# [1]
foo() # [1]
foo() # [1]

print(foo.__defaults__) # ([None],)

就目前,很好很直观。接下来你会发现,若不了解默认值的工作方式,ES5语义上会产生一些困惑。

外层作用域的遮蔽

来看下面的例子:

var x = 1;

function foo(x, y = x) {
console.log(y);
}

foo(
2); // 2, 不是 1!
来上例的y输出结果看起来像是1,但实际上是2,不是1。原因是参数中的x与全局的x不同。由于默认值在函数调用时求值,所以当赋值=x时,x已经在内部作用域决定了,引用的是参数x本身。也就是说,参数x被全局的同名变量遮蔽,所以每次默认值中访问x时,实际访问到的是参数中的x

参数的TDZ(Temporal Dead Zone,暂存死区)

ES6提到所谓的TDZ(暂存死区),意指这样的程序区域:初始化前的变量或参数不能被访问。

考虑到对于参数,不能将自己作为默认值:

var x = 1;

function foo(x = x) { // throws!
...
}
赋值=x正如我们上面提到的那样,x会被解释为参数级作用域中的x,而全局的x会被遮蔽。但是,x位于TDZ,在初始化前不能被访问。因此,它不能自己初始化自己。

注意,上面之前的例子中的<

阅读更多内容

没有评论:

发表评论