纯JS代码绘制3D红色玫瑰:蒙特卡罗与Z-Buffer技术解析
本文介绍如何使用纯JavaScript脚本在浏览器中绘制3D红色玫瑰。通过结合蒙特卡罗采样、Z-Buffer隐藏面消除及欧拉旋转技术,利用Canvas实现高性能图形渲染。文章详细解析了从几何建模到色彩填充的完整实现过程,并提供核心代码供开发者参考。
纯JS代码绘制3D红色玫瑰概述
Roman Cortes 发布了一段使用 JavaScript 编写的红色玫瑰花生成脚本。这种用代码生成的玫瑰花,被认为是程序员送给女友的最佳情人节礼物。需要注意的是,不同浏览器下的渲染效果和运行速度可能存在显著差异。

该图片由代码实时生成,用户可以刷新页面以重复观看玫瑰花的绘制过程。
以下是实现3D玫瑰花效果的核心代码:
代码如下:
with(m=Math)C=cos,S=sin,P=pow,R=random;c.width=c.height=f=500;h=-250;function p(a,b,c){if(c>60)return[S(a*7)*(13+5/(.2+P(b*4,4)))-S(b)*50,b*f+50,625+C(a*7)*(13+5/(.2+P(b*4,4)))+b*400,a*1-b/2,a];A=a*2-1;B=b*2-1;if(A*A+B*B<1){if(c>37){n=(j=c&1)?6:4;o=.5/(a+.01)+C(b*125)*3-a*300;w=b*h;return[o*C(n)+w*S(n)+j*610-390,o*S(n)-w*C(n)+550-j*350,1180+C(B+A)*99-j*300,.4-a*.1+P(1-B*B,-h*6)*.15-a*b*.4+C(a+b)/5+P(C((o*(a+1)+(B>0?w:-w))/25),30)*.1*(1-B*B),o/1e3+.7-o*w*3e-6]}if(c>32){c=c*1.16-.15;o=a*45-20;w=b*b*h;z=o*S(c)+w*C(c)+620;return[o*C(c)-w*S(c),28+C(B*.5)*99-b*b*b*60-z/2-h,z,(b*b*.3+P((1-(A*A)),7)*.15+.3)*b,b*.7]}o=A*(2-b)*(80-c*2);w=99-C(A)*120-C(b)*(-h-c*4.9)+C(P(1-b,7))*50+c*2;z=o*S(c)+w*C(c)+700;return[o*C(c)-w*S(c),B*99-C(P(b, 7))*50-c/3-z/1.35+450,z,(1-b/1.2)*.9+a*.1, P((1-b),20)/4+.05]}}setInterval('for(i=0;i<1e4;i++)if(s=p(R(),R(),i%46/.74)){z=s[2];x=~~(s[0]*f/z-h);y=~~(s[1]*f/z-h);if(!m[q=y*f+x]|m[q]>z)m[q]=z,a.fillStyle="rgb("+~(s[3]*h)+","+~(s[4]*h)+","+~(s[3]*s[3]*-80)+")",a.fillRect(x,y,1,1)}',0)
外观采样与呈现效果绘制
对实现原理感兴趣的读者可以参考以下关于实现过程及相关理论的详细说明。
该3D代码玫瑰的视觉效果采用了蒙特卡罗方法。创作者高度评价该方法,认为其在功能优化和采样方面是“令人难以置信的强大工具”。关于蒙特卡罗方法的更多参考信息可查阅 Monte Carlo method。
具体操作步骤如下:
外观采样与呈现效果绘制
作者使用了多种不同的形状图来组合构成这朵代码玫瑰。总共使用了 31 个形状:包括 24 个花瓣、4 个萼片、2 个叶子和 1 根花茎,每个形状均通过代码进行描绘。
首先,定义一个采样范围:
function surface(a, b) { // I'm using a and b as parameters ranging from 0 to 1.
return {
x: a*50,
y: b*50
};
// this surface will be a square of 50x50 units of size
}
然后,编写形状描绘代码:
var canvas = document.body.appendChild(document.createElement("canvas")),
context = canvas.getContext("2d"),
a, b, position;
// Now I'm going to sample the surface at .1 intervals for a and b parameters:
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);
}
}

接下来,尝试使用更密集的采样间隔:

随着采样间隔变得愈发密集,点与点之间的距离逐渐缩小。当密度达到最高时,相邻点间距小于一个像素,肉眼便无法察觉间隔(参见 0.01 的效果)。为避免产生过大的视觉差异,进一步缩小采样间隔,此时绘制区域已完全填满(对比 0.01 和 0.001 的结果)。
随后,使用以下公式绘制圆形:(X-X0)^ 2 +(Y-Y0)^ 2 <半径^ 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) {
// inside the circle
return {
x: x,
y: y
};
} else {
// outside the circle
return null;
}
}
为防止溢出,还需添加一个采样条件判断:
if (position = surface(a, b)) {
context.fillRect(position.x, position.y, 1, 1);
}
定义圆形有多种方法,其中部分无需使用拒绝采样。作者并未限定必须使用某种定义方式,因此下面展示另一种定义圆形的方法:
function surface(a, b) {
// Circle using polar coordinates
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
};
}
(此方法相比前一种方法需要更密集的采样以完成填充。)
现在,对圆形进行变形处理,使其看起来更像花瓣:
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 // deformation
};
} else {
return null;
}
}
这看起来已经非常接近玫瑰花瓣的形状。此时,可以尝试修改部分函数数值,从而生成许多有趣的形状。
接下来,为图形添加色彩:
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), // this will add a gradient
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);
}
}
}
至此,一片带色的花瓣便呈现出来。
3D曲面与透视投影技术
3D曲面与透视投影
定义三维曲面相对简单,例如,可以定义一个管状物体:
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, // by subtracting length/2 I have centered the tube at (0, 0, 0)
r: 0,
g: Math.floor(b * 255),
b: 0
};
}
接着添加投影透视图,首先需要定义一个摄像头位置:

如上图所示,将摄像头放置在(0,0,Z)位置,画布位于 X/Y 平面。投影到画布上的采样点计算如下:
var pX, pY, // projected on canvas x and y coordinates
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);
}
}
}
最终效果如下:

Z-Buffer(深度缓冲)
Z-Buffer 是计算机图形学中一项普遍技术,在物体着色时执行“隐藏面消除”操作,确保被遮挡的物体部分不会被显示出来。

上图展示了使用 Z-Buffer 技术处理后的玫瑰效果(可以看到已具备立体感)。
相关代码如下:
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);
}
}
}
}
旋转处理
可以使用任何矢量旋转方法。在代码玫瑰的创建中,作者使用的是欧拉旋转。现在将之前编写的管状物进行旋转,实现绕 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
};
}
旋转效果如下:

蒙特卡罗方法与随机数处理
蒙特卡罗方法应用
关于采样时间,间隔过大或过小都会导致极差的视觉感受,因此需要设置合理的采样间隔,这里采用蒙特卡罗方法。
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 为随机参数,通过足够的采样完成表面填充。每次绘制 10000 个点,然后等待屏幕完成更新。
另外需要注意的是,如果随机数生成出现错误,表面填充效果也会出错。在某些浏览器中,Math.random 的执行可能是线性的,这可能导致表面填充效果异常。此时,需要使用类似 Mersenne Twister(一种随机数算法)的高质量 PRNG 采样来避免错误发生。
完成同步
为了使玫瑰的每个部分在同一时间完成并呈现,还需要添加一个功能,为每部分设置一个参数以返回值进行同步。并使用一个分段函数代表玫瑰的各个部分。例如在花瓣部分,作者通过旋转和变形来创建它们。
虽然表面采样方法是创建三维图形非常著名且古老的方法之一,但将蒙特卡罗与 Z-Buffer 结合到表面采样中的方法并不常见。对于现实生活场景的制作,这或许算不上极具创意,但其简易的代码实现和极小的体积仍令人满意。
希望这篇文章能激发计算机图形学爱好者尝试不同的呈现方法,并从中获得乐趣。(Roman Cortes)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
