译自:[http://www.romancortes.com/blog/1k-rose/](http://www.romancortes.com/blog/1k-rose/)
转载请标明作者和出处:[http://blog.csdn.net/hfahe](http://blog.csdn.net/hfahe)
![](https://box.kancloud.cn/2016-08-09_57a9a2ddad2ee.gif)
我曾参与[js1k](http://js1k.com/2012-love/)爱情主题的第四次活动(译者注:关于有趣的js1k,可以看看我上一篇博文《[JS1k比赛与3D玫瑰](http://blog.csdn.net/hfahe/article/details/7249682)》)。我所提交的是一个静态图像,由程序生成的三维玫瑰。你可以在[这里](http://js1k.com/2012-love/demo/1022)看到它。
它是通过显式分段三维曲面的蒙特卡洛采样所实现的。我要在这篇文章中尝试解释所有内容。
**关于蒙特卡罗方法的简短说明**
蒙特卡罗方法是令人难以置信的强大工具。我一直在使用它们来实现很多功能优化和采样的问题。相比起设计和编写算法,如果你有更多CPU时间的话,它们几乎可以像变魔术一样。在这个关于玫瑰的案例里,它对于代码大小的优化非常有用。
如果你对于蒙特卡罗方法了解不多,你可以读读[Wikipedia上一篇不错的相关文章](http://en.wikipedia.org/wiki/Monte_carlo_method)。
**显式的曲面和采样/绘图**
我使用多个显式定义的曲面来定义玫瑰的形状。我总共使用了31个面:24片花瓣,4片萼片(花瓣周围的薄叶),2片叶子以及玫瑰枝。
那么它们是如何表示这些显式曲面的呢?这非常容易,我将会提供一个二维的例子:
首先,我定义了显式曲面的函数:
~~~
function surface(a, b) { // 我使用0到1之间的a和b作为参数
return {
x: a*50,
y: b*50
};
// 曲面将会是50*50大小的一个正方形
}
~~~
然后下面是绘制它的代码:
~~~
var canvas = document.body.appendChild(document.createElement("canvas")),
context = canvas.getContext("2d"),
a, b, position;
// 现在我将要为a和b参数采用0.1间隔来进行曲面采样
for (a = 0; a < 1; a += .1) {
for (b = 0; b < 1; b += .1) {
position = surface(a, b);
context.fillRect(position.x, position.y, 1, 1);
}
}
~~~
下面是结果:
![](https://box.kancloud.cn/2016-08-09_57a9a2dded1ac.gif)
放大了4倍
现在,让我们尝试更密集的采样间隔(更小的间隔=更密集的采样):
![](https://box.kancloud.cn/2016-08-09_57a9a2de0da19.gif)
正如你看到的一样,因为你的样例越来越密集,点越来越接近,当相邻两点的距离小于一个像素时,屏幕上区域已经被完全的充满了(见0.01的图)。之后使它更密集并不会造成太大的视觉差别,因为你只是在已经填满的区域上继续绘制(比较0.01和0.001的结果)。
OK,现在让我们重新定义曲面函数来画一个圆。 有多种方法可以做到这一点,但我会用这个公式:(x-x0)^2 + (y-y0)^2 <radius^2,其中(x0, y0) 是圆心:
~~~
function surface(a, b) {
var x = a * 100,
y = b * 100,
radius = 50,
x0 = 50,
y0 = 50;
if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {
// 圆内
return {
x: x,
y: y
};
} else {
// 圆外
return null;
}
}
~~~
因为不允许圆外有点,我应该添加在抽样时添加条件:
~~~
if (position = surface(a, b)) {
context.fillRect(position.x, position.y, 1, 1);
}
~~~
效果如下:
![](https://box.kancloud.cn/2016-08-09_57a9a2de22d79.gif)
正如我前面所说的一样,有许多不同的方法来定义一个圆,其中一些并不需要拒绝抽样。我将要展示一种方式,但只是作为一个提示,我不会在后面的文章中继续采用:
~~~
function surface(a, b) {
var angle = a * Math.PI * 2,
radius = 50,
x0 = 50,
y0 = 50;
return {
x: Math.cos(angle) * radius * b + x0,
y: Math.sin(angle) * radius * b + y0
};
}
~~~
![](https://box.kancloud.cn/2016-08-09_57a9a2de22d79.gif)
(此方法比起之前的需要一个更密集的取样来填充这个圆)
好了,现在让圆变形让它看起来更像是一个花瓣:
~~~
function surface(a, b) {
var x = a * 100,
y = b * 100,
radius = 50,
x0 = 50,
y0 = 50;
if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {
return {
x: x,
y: y * (1 + b) / 2 // 变形
};
} else {
return null;
}
}
~~~
效果如下:
![](https://box.kancloud.cn/2016-08-09_57a9a2de40e67.gif)
好了,现在这看起来很像一个玫瑰花瓣的形状。我建议你应用一点变形。您可以使用任何可以想得到的数学函数,例如加,减,乘,除,SIN,COS,开方......等等。只需要试验修改一点函数,就会出现很多形状(一些会更有趣,一些则不)。
现在我想给它添加一些颜色,所以我要添加曲面颜色数据:
~~~
function surface(a, b) {
var x = a * 100,
y = b * 100,
radius = 50,
x0 = 50,
y0 = 50;
if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {
return {
x: x,
y: y * (1 + b) / 2,
r: 100 + Math.floor((1 - b) * 155), // 这将添加一个渐变
g: 50,
b: 50
};
} else {
return null;
}
}
for (a = 0; a < 1; a += .01) {
for (b = 0; b < 1; b += .001) {
if (point = surface(a, b)) {
context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
context.fillRect(point.x, point.y, 1, 1);
}
}
}
~~~
效果如下:
![](https://box.kancloud.cn/2016-08-09_57a9a2de51c52.jpg)
这里就是一个带颜色的花瓣了!
**3D曲面和透视投影**
定义三维曲面很简单:只需要为曲面函数添加一个Z属性。作为一个示例,我将要定义一个管道/圆柱体:
~~~
function surface(a, b) {
var angle = a * Math.PI * 2,
radius = 100,
length = 400;
return {
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius,
z: b * length - length / 2, // 通过剪掉lenght/2,我把管道的中心放在(0,0,0)
r: 0,
g: Math.floor(b * 255),
b: 0
};
}
~~~
现在,为了添加透视投影,第一步我们要定义一个相机:
![](https://box.kancloud.cn/2016-08-09_57a9a2de64013.gif)
我将把相机放置在坐标(0,0,cameraZ)上并把从相机到画布的距离称为“透视”。我会考虑到画布是在X / Y平面,以(0,0,cameraZ +透视)为中心。现在每个采样点将会被投影到画布上:
~~~
var pX, pY, // 设计画布x和y坐标
perspective = 350,
halfHeight = canvas.height / 2,
halfWidth = canvas.width / 2,
cameraZ = -700;
for (a = 0; a < 1; a += .001) {
for (b = 0; b < 1; b += .01) {
if (point = surface(a, b)) {
pX = (point.x * perspective) / (point.z - cameraZ) + halfWidth;
pY = (point.y * perspective) / (point.z - cameraZ) + halfHeight;
context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
context.fillRect(pX, pY, 1, 1);
}
}
}
~~~
这将有如下的效果:
![](https://box.kancloud.cn/2016-08-09_57a9a2de79137.jpg)
**Z-缓冲**
Z-缓冲在计算机图形学上是非常常见的技术,用于通过已经绘制过更远的点来绘制离相机更近的点。它的工作原理是维持一个数组来记录图像上所有靠近z轴的点。
![](https://box.kancloud.cn/2016-08-09_57a9a2de8b536.jpg)
这是可视化Z-缓冲的玫瑰,黑色表示远离相机,白色表示靠近相机。
实现如下:
~~~
var zBuffer = [],
zBufferIndex;
for (a = 0; a < 1; a += .001) {
for (b = 0; b < 1; b += .01) {
if (point = surface(a, b)) {
pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth);
pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight);
zBufferIndex = pY * canvas.width + pX;
if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) {
zBuffer[zBufferIndex] = point.z;
context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
context.fillRect(pX, pY, 1, 1);
}
}
}
}
~~~
**旋转圆柱体**
你可以使用任何矢量旋转的方法。在这个例子里,我使用[欧拉旋转](http://en.wikipedia.org/wiki/Euler_angles)。让我们来实现绕Y轴旋转:
~~~
function surface(a, b) {
var angle = a * Math.PI * 2,
radius = 100,
length = 400,
x = Math.cos(angle) * radius,
y = Math.sin(angle) * radius,
z = b * length - length / 2,
yAxisRotationAngle = -.4, // in radians!
rotatedX = x * Math.cos(yAxisRotationAngle) + z * Math.sin(yAxisRotationAngle),
rotatedZ = x * -Math.sin(yAxisRotationAngle) + z * Math.cos(yAxisRotationAngle);
return {
x: rotatedX,
y: y,
z: rotatedZ,
r: 0,
g: Math.floor(b * 255),
b: 0
};
}
~~~
效果如下:
![](https://box.kancloud.cn/2016-08-09_57a9a2de9bf64.jpg)
**蒙特卡罗抽样**
我一直在这篇文章中使用基于间隔的采样。它需要为每个曲面设置适当的时间间隔。如果间隔大,渲染的速度快,但是曲面上会出现未完全填充的洞。另一方面,如果时间间隔太小,渲染的时间会增长为一个惊人的数量。
所以,让我们切换到蒙特卡罗抽样:
~~~
var i;
window.setInterval(function () {
for (i = 0; i < 10000; i++) {
if (point = surface(Math.random(), Math.random())) {
pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth);
pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight);
zBufferIndex = pY * canvas.width + pX;
if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) {
zBuffer[zBufferIndex] = point.z;
context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
context.fillRect(pX, pY, 1, 1);
}
}
}
}, 0);
~~~
现在A和B参数被设置为2个随机值。通过足够多点的采样,我们填充了曲面。感谢采样间隔,我可以每次绘制10000个点,然后让屏幕刷新。
另外需要说明的是,只有随机数生成器足够好才能保证曲面的充分填充。在一些浏览器中,Math.random通过[线性同余生成器](http://en.wikipedia.org/wiki/Linear_congruential_generator)来实现 ,这可能会导致曲面产生一些问题。如果你需要一个好的PRNG采样,可以使用一些高质量的方式例如Mersenne Twister(它有JS的实现),或者使用一些浏览器提供的密码随机生成器。使用[低差异序列](http://en.wikipedia.org/wiki/Low-discrepancy_sequence)也是非常可取的。
**最后的事项**
要完成这个玫瑰,玫瑰的每个部分,每一个曲面,都会在同一时间统一进行渲染。我为选择玫瑰一部分的函数添加另外一个参数来返回一个点。从数学来说这是一个分段函数,其中每一块各代表了玫瑰的一部分。对花瓣来说,我用旋转和伸展/变形来创建所有的花瓣。所有的一切都是通过文章中所介绍的概念来综合实现的。
虽然显式曲面抽样是一个相当有名,和3D图形学最古老的方法之一,但是像我这样艺术的使用分段/蒙特卡洛/Z-缓冲已经相当少见了。不算非常有创意,在现实生活场景中可能也不算有用,但它非常适合js1k这种要求简单和文件大小的环境。
我真的希望能通过这篇文章激发读者对计算机图形学的兴趣,并乐于尝试所有不同的渲染方式。图形学里包含一整个世界,在其中进行探索和发挥是相当美妙的。
- 前言
- AutoPager的简单实现
- 利用CSS3特性巧妙实现漂亮的DIV箭头
- IE9在Win7下任务栏新特性简介
- 浏览器九宫格的简单实现
- Raphael js库简介
- 使用CSS3构建Ajax加载动画
- 用CSS3创建动画价格表
- 用CSS3实现浏览器的缩放功能
- 用纯CSS3实现QQ LOGO
- 用CSS3创建旋转载入器
- 使用Javascript开发移动应用程序
- 用HTML5创建超酷图像灰度渐变效果
- 使用CSS3创建文字颜色渐变(CSS3 Text Gradient)
- 仅用CSS创建立体旋转幻灯片
- 如何创建跨浏览器的HTML5表单
- 用CSS3实现动画进度条
- HTML5 Guitar Tab Player
- 奇妙的HTML5 Canvas动画实例
- 谈HTML5和CSS3的国际化支持
- 实现跨浏览器的HTML5占位符
- 前端开发必备工具:WhatFont Bookmarklet-方便的查询网页上的字体
- 使用HTML5和CSS3来创建幻灯片
- HTML5之美
- 如何使用HTML5创建在线精美简历
- 以小见大、由浅入深-谈如何面试Javascript工程师
- 快速入门:HTML5强大的Details元素
- 用CSS3实现图像风格
- HTML5视频字幕与WebVTT
- 用纯CSS3实现Path华丽动画
- 用3个步骤实现响应式网页设计
- 遇见CSS3滤镜
- 关于CSS3滤镜的碎念
- 用纯CSS3绘制萌系漫画人物动态头像
- CSS3新的鼠标样式介绍
- 用HTML5献上爱的3D玫瑰
- 对HTML5 Device API相关规范的解惑
- 如何使用HTML5实现拍照上传应用
- 2012第一季度国外HTML5移动开发趋势
- HTML5新特性:范围样式
- 百度开发者大会-《用HTML5新特性开发移动App》PPT分享
- Chrome 19对于HTML5最新支持的动态:电池状态API,全屏API,震动API,语音API
- 遇见Javascript类型数组(Typed Array)
- 用HTML5 Audio API开发游戏音乐
- 用HTML5实现人脸识别
- 用Javascript实现人脸美容
- Chrome 20对于HTML5最新支持的动态:颜色输入,网络信息API,CSS着色器
- 用HTML5实现手机摇一摇的功能
- 用HTML5实现iPad应用无限平滑滚动
- 用非响应式设计构建跨端Web App
- 了解SVG
- HTML5图像适配介绍
- HTML5安全:内容安全策略(CSP)简介
- HTML5安全:CORS(跨域资源共享)简介
- 用CSS3 Region和3D变换实现书籍翻页效果
- 谈谈移动App的思维误区
- Chrome新特性:文件夹拖拽支持
- 《关注HTML5安全》
- HTML5安全风险详析之一:CORS攻击
- HTML5安全风险详析之二:Web Storage攻击
- HTML5图像适配最新进展:响应式图片规范草案
- HTML5移动Web App相关标准状态及路线图
- HTML5安全风险详析之三:WebSQL攻击
- Chrome引入WebRTC支持视频聊天App
- HTML5安全风险详析之四:Web Worker攻击
- HTML5安全风险详析之五:劫持攻击
- HTML5安全风险详析之六:API攻击
- HTML5安全攻防详析之七:新标签攻击
- 在iOS Safari中播放离线音频
- 使用WebRTC实现远程屏幕共享
- Firefox、Android、iOS遇见WebRTC
- HTML5光线传感器简介
- HTML5安全攻防详析之八:Web Socket攻击
- HTML5安全攻防详析之完结篇:HTML5对安全的改进
- 激动人心!在网页上通过语音输入文字 - HTML5 Web Speech API介绍
- Web滚动性能优化实战
- 用CSS3设计响应式导航菜单
- 用HTML5构建高性能视差网站
- 漫谈@supports与CSS3条件规则
- HTML5下载属性简介
- 如何开发优秀的HTML5游戏?-迪斯尼《寻找奥兹之路》游戏技术详解(一)
- 如何开发优秀的HTML5游戏?-迪斯尼《寻找奥兹之路》游戏技术详解(二)
- 趋势:Chrome为打包应用提供强大新特性
- 从HTML5移动应用现状谈发展趋势
- 基于HTML5的Web跨设备超声波通信方案