用HTML5献上爱的3D玫瑰

为之折服!
译自: http://www.romancortes.com/blog/1k-rose/

转载请标明作者和出处:http://blog.csdn.net/hfahe


        我曾参与js1k爱情主题的第四次活动(译者注:关于有趣的js1k,可以看看我上一篇博文《JS1k比赛与3D玫瑰》)。我所提交的是一个静态图像,由程序生成的三维玫瑰。你可以在这里看到它。

        它是通过显式分段三维曲面的蒙特卡洛采样所实现的。我要在这篇文章中尝试解释所有内容。

关于蒙特卡罗方法的简短说明

        蒙特卡罗方法是令人难以置信的强大工具。我一直在使用它们来实现很多功能优化和采样的问题。相比起设计和编写算法,如果你有更多CPU时间的话,它们几乎可以像变魔术一样。在这个关于玫瑰的案例里,它对于代码大小的优化非常有用。

        如果你对于蒙特卡罗方法了解不多,你可以读读Wikipedia上一篇不错的相关文章

显式的曲面和采样/绘图

        我使用多个显式定义的曲面来定义玫瑰的形状。我总共使用了31个面:24片花瓣,4片萼片(花瓣周围的薄叶),2片叶子以及玫瑰枝。

        那么它们是如何表示这些显式曲面的呢?这非常容易,我将会提供一个二维的例子:

        首先,我定义了显式曲面的函数:

[html]  view plain copy
  1. function surface(a, b) {  // 我使用0到1之间的a和b作为参数  
  2.     return {  
  3.         x: a*50,  
  4.         y: b*50  
  5.     };  
  6.     // 曲面将会是50*50大小的一个正方形  
  7. }  

        然后下面是绘制它的代码:

[html]  view plain copy
  1. var canvas = document.body.appendChild(document.createElement("canvas")),  
  2.     context = canvas.getContext("2d"),  
  3.     a, b, position;  
  4.   
  5. // 现在我将要为a和b参数采用0.1间隔来进行曲面采样  
  6.   
  7. for (a = 0; a < 1; a += .1) {  
  8.     for (b = 0; b < 1; b += .1) {  
  9.         position = surface(a, b);  
  10.         context.fillRect(position.x, position.y, 1, 1);  
  11.     }  
  12. }  

        下面是结果:


放大了4倍

        现在,让我们尝试更密集的采样间隔(更小的间隔=更密集的采样):


        正如你看到的一样,因为你的样例越来越密集,点越来越接近,当相邻两点的距离小于一个像素时,屏幕上区域已经被完全的充满了(见0.01的图)。之后使它更密集并不会造成太大的视觉差别,因为你只是在已经填满的区域上继续绘制(比较0.01和0.001的结果)。

        OK,现在让我们重新定义曲面函数来画一个圆。 有多种方法可以做到这一点,但我会用这个公式:(x-x0)^2 + (y-y0)^2 <radius^2,其中(x0, y0) 是圆心:

[html]  view plain copy
  1. function surface(a, b) {  
  2.     var x = a * 100,  
  3.         y = b * 100,  
  4.         radius = 50,  
  5.         x0 = 50,  
  6.         y0 = 50;  
  7.   
  8.     if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {  
  9.         // 圆内  
  10.         return {  
  11.             x: x,  
  12.             y: y  
  13.         };  
  14.     } else {  
  15.         // 圆外  
  16.         return null;  
  17.     }  
  18. }  

        因为不允许圆外有点,我应该添加在抽样时添加条件:

[html]  view plain copy
  1. if (position = surface(a, b)) {  
  2.     context.fillRect(position.x, position.y, 1, 1);  
  3. }  

        效果如下:


        正如我前面所说的一样,有许多不同的方法来定义一个圆,其中一些并不需要拒绝抽样。我将要展示一种方式,但只是作为一个提示,我不会在后面的文章中继续采用:

[html]  view plain copy
  1. function surface(a, b) {  
  2.     var angle = a * Math.PI * 2,  
  3.         radius = 50,  
  4.         x0 = 50,  
  5.         y0 = 50;  
  6.   
  7.     return {  
  8.         x: Math.cos(angle) * radius * b + x0,  
  9.         y: Math.sin(angle) * radius * b + y0  
  10.     };  
  11. }  
        (此方法比起之前的需要一个更密集的取样来填充这个圆)

        好了,现在让圆变形让它看起来更像是一个花瓣:

[html]  view plain copy
  1. function surface(a, b) {  
  2.     var x = a * 100,  
  3.         y = b * 100,  
  4.         radius = 50,  
  5.         x0 = 50,  
  6.         y0 = 50;  
  7.   
  8.     if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {  
  9.         return {  
  10.             x: x,  
  11.             y: y * (1 + b) / 2 // 变形  
  12.         };  
  13.     } else {  
  14.         return null;  
  15.     }  
  16. }  

        效果如下:


        好了,现在这看起来很像一个玫瑰花瓣的形状。我建议你​​应用一点变形。您可以使用任何可以想得到的数学函数,例如加,减,乘,除,SIN,COS,开方......等等。只需要试验修改一点函数,就会出现很多形状(一些会更有趣,一些则不)。

        现在我想给它添加一些颜色,所以我要添加曲面颜色数据:

[html]  view plain copy
  1. function surface(a, b) {  
  2.     var x = a * 100,  
  3.         y = b * 100,  
  4.         radius = 50,  
  5.         x0 = 50,  
  6.         y0 = 50;  
  7.   
  8.     if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {  
  9.         return {  
  10.             x: x,  
  11.             y: y * (1 + b) / 2,  
  12.             r: 100 + Math.floor((1 - b) * 155), // 这将添加一个渐变  
  13.             g: 50,  
  14.             b: 50  
  15.         };  
  16.     } else {  
  17.         return null;  
  18.     }  
  19. }  
  20.   
  21. for (a = 0; a < 1; a += .01) {  
  22.     for (b = 0; b < 1; b += .001) {  
  23.         if (point = surface(a, b)) {  
  24.             context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";  
  25.             context.fillRect(point.x, point.y, 1, 1);  
  26.         }  
  27.     }  
  28. }  

        效果如下:


        这里就是一个带颜色的花瓣了!

3D曲面和透视投影

        定义三维曲面很简单:只需要为曲面函数添加一个Z属性。作为一个示例,我将要定义一个管道/圆柱体:

[html]  view plain copy
  1. function surface(a, b) {  
  2.     var angle = a * Math.PI * 2,  
  3.         radius = 100,  
  4.         length = 400;  
  5.   
  6.     return {  
  7.         x: Math.cos(angle) * radius,  
  8.         y: Math.sin(angle) * radius,  
  9.         z: b * length - length / 2, // 通过剪掉lenght/2,我把管道的中心放在(0,0,0)  
  10.         r: 0,  
  11.         g: Math.floor(b * 255),  
  12.         b: 0  
  13.     };  
  14. }  

        现在,为了添加透视投影,第一步我们要定义一个相机:


        我将把相机放置在坐标(0,0,cameraZ)上并把从相机到画布的距离称为“透视”。我会考虑到画布是在X / Y平面,以(0,0,cameraZ +透视)为中心。现在每个采样点将会被投影到画布上:

[html]  view plain copy
  1. var pX, pY,  // 设计画布x和y坐标  
  2.     perspective = 350,  
  3.     halfHeight = canvas.height / 2,  
  4.     halfWidth = canvas.width / 2,  
  5.     cameraZ = -700;  
  6.   
  7. for (a = 0; a < 1; a += .001) {  
  8.     for (b = 0; b < 1; b += .01) {  
  9.         if (point = surface(a, b)) {  
  10.             pX = (point.x * perspective) / (point.z - cameraZ) + halfWidth;  
  11.             pY = (point.y * perspective) / (point.z - cameraZ) + halfHeight;  
  12.             context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";  
  13.             context.fillRect(pX, pY, 1, 1);  
  14.         }  
  15.     }  
  16. }  

        这将有如下的效果:


Z-缓冲

        Z-缓冲在计算机图形学上是非常常见的技术,用于通过已经绘制过更远的点来绘制离相机更近的点。它的工作原理是维持一个数组来记录图像上所有靠近z轴的点。


        这是可视化Z-缓冲的玫瑰,黑色表示远离相机,白色表示靠近相机。

        实现如下:

[html]  view plain copy
  1. var zBuffer = [],  
  2.     zBufferIndex;  
  3.   
  4. for (a = 0; a < 1; a += .001) {  
  5.     for (b = 0; b < 1; b += .01) {  
  6.         if (point = surface(a, b)) {  
  7.             pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth);  
  8.             pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight);  
  9.             zBufferIndex = pY * canvas.width + pX;  
  10.             if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) {  
  11.                 zBuffer[zBufferIndex] = point.z;  
  12.                 context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";  
  13.                 context.fillRect(pX, pY, 1, 1);  
  14.             }  
  15.         }  
  16.     }  
  17. }  

旋转圆柱体

        你可以使用任何矢量旋转的方法。在这个例子里,我使用欧拉旋转。让我们来实现绕Y轴旋转:

[html]  view plain copy
  1. function surface(a, b) {  
  2.     var angle = a * Math.PI * 2,  
  3.         radius = 100,  
  4.         length = 400,  
  5.         x = Math.cos(angle) * radius,  
  6.         y = Math.sin(angle) * radius,  
  7.         z = b * length - length / 2,  
  8.         yAxisRotationAngle = -.4, // in radians!  
  9.         rotatedX = x * Math.cos(yAxisRotationAngle) + z * Math.sin(yAxisRotationAngle),  
  10.         rotatedZ = x * -Math.sin(yAxisRotationAngle) + z * Math.cos(yAxisRotationAngle);  
  11.   
  12.     return {  
  13.         x: rotatedX,  
  14.         y: y,  
  15.         z: rotatedZ,  
  16.         r: 0,  
  17.         g: Math.floor(b * 255),  
  18.         b: 0  
  19.     };  
  20. }  

        效果如下:


蒙特卡罗抽样

        我一直在这篇文章中使用基于间隔的采样。它需要为每个曲面设置适当的时间间隔。如果间隔大,渲染的速度快,但是曲面上会出现未完全填充的洞。另一方面,如果时间间隔太小,渲染的时间会增长为一个惊人的数量。

        所以,让我们切换到蒙特卡罗抽样:

[html]  view plain copy
  1. var i;  
  2.   
  3. window.setInterval(function () {  
  4.     for (i = 0; i < 10000; i++) {  
  5.         if (point = surface(Math.random(), Math.random())) {  
  6.             pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth);  
  7.             pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight);  
  8.             zBufferIndex = pY * canvas.width + pX;  
  9.             if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) {  
  10.                 zBuffer[zBufferIndex] = point.z;  
  11.                 context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";  
  12.                 context.fillRect(pX, pY, 1, 1);  
  13.             }  
  14.         }  
  15.     }  
  16. }, 0);  

        现在A和B参数被设置为2个随机值。通过足够多点的采样,我们填充了曲面。感谢采样间隔,我可以每次绘制10000个点,然后让屏幕刷新。

        另外需要说明的是,只有随机数生成器足够好才能保证曲面的充分填充。在一些浏览器中,Math.random通过线性同余生成器来实现 ,这可能会导致曲面产生一些问题。如果你需要一个好的PRNG采样,可以使用一些高质量的方式例如Mersenne Twister(它有JS的实现),或者使用一些浏览器提供的密码随机生成器。使用低差异序列也是非常可取的。

最后的事项

        要完成这个玫瑰,玫瑰的每个部分,每一个曲面,都会在同一时间统一进行渲染。我为选择玫瑰一部分的函数添加另外一个参数来返回一个点。从数学来说这是一个分段函数,其中每一块各代表了玫瑰的一部分。对花瓣来说,我用旋转和伸展/变形来创建所有的花瓣。所有的一切都是通过文章中所介绍的概念来综合实现的。

        虽然显式曲面抽样是一个相当有名,和3D图形学最古老的方法之一,但是像我这样艺术的使用分段/蒙特卡洛/Z-缓冲已经相当少见了。不算非常有创意,在现实生活场景中可能也不算有用,但它非常适合js1k这种要求简单和文件大小的环境。

        我真的希望能通过这篇文章激发读者对计算机图形学的兴趣,并乐于尝试所有不同的渲染方式。图形学里包含一整个世界,在其中进行探索和发挥是相当美妙的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值