Canvas / 3D Роза методом Монте-Карло

в 9:14, , рубрики: canvas, javascript, js1k, surface, монте-карло, метки: , , , ,

Canvas / 3D Роза методом Монте-Карло

В этой статье я надеюсь вдохновить читателей, интересующихся компьютерной графикой, чтобы экспериментировать и весело провести время с различными методами визуализации.Roman Cortes для конкурса любви 2012 js1k сделал 3D розу на javascript (canvas), используя метод Монте-Карло.
Кратко о методе Монте-Карло
Ме́тод Мо́нте-Ка́рло — общее название группы численных методов, основанных на получении большого числа реализаций стохастического (случайного) процесса, который формируется таким образом, чтобы его вероятностные характеристики совпадали с аналогичными величинами решаемой задачи. Используется для решения задач в различных областях физики, химии, математики, экономики, оптимизации, теории управления и др.
Отрисовка поверхностей

Чтобы определить форму розы я использую несколько поверхностей. В общей сложности это 31 поверхность: 24 лепестка, 4 чашелистика (тонкие листья вокруг лепестков), 2 листа и 1 стебель розы.
2d пример функции определения поверхности:
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);
}
}

Результат:
Сейчас мы уменьшим интервал этих пикселей для более плотной картинки
Это позволяет переопределить функцию поверхности для отрисовки круга. Есть несколько способов сделать это, но я буду использовать следующую формулу: (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) {
// 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 поверхность очень просто: просто добавьте a 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, // by subtracting length/2 I have centered the tube at (0, 0, 0)
r: 0,
g: Math.floor(b * 255),
b: 0
};
}

Для добавления перспективы проекции надо определить положение камеры
Я буду делать мою камеру в точке (0, 0, cameraZ), называться она будет «perspective» — расстояние от камеры к холсту. Центр будет в точке (0, 0, cameraZ + perspective). Теперь каждая точка будет проецироваться на холсте:
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-буфер является довольно распространенной технологией в области компьютерной графики для учета удаленности элементов изображения.
Это видно на 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);
}
}
}
}

Вращение цилиндра

Вы можете использовать любой метод вектора вращения. В случае с розой, я использую теорему вращения Эйлера. Давайте реализуем вращение вокруг оси 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);

Заключение

Чтобы завершить розу, я добавил третий параметр для функции которая выбирает часть розы. В случае c лепестками, я использовал повороты и растяжки/деформации. Все это делается путем смешивания, которые описаны в этой статье.
Мой выбор Монте-Карло/Z-буферизации был выбран для художественных целей. Не очень инновационный и не очень полезный скрипт, но очень хорошо вписывается в контекст js1k, где простота и минимальные размеры желательны.
Даже не верится что вся роза может находится в этом коде:

JS1k, 1k demo submission [1022]

var b = document.body;
var c = document.getElementsByTagName('canvas')[0];
var a = c.getContext('2d');
document.body.clientWidth; // fix bug in webkit: http://qfox.nl/weblog/218

// start of submission //
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*B37){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©+w*C©+620;return[o*C©-w*S©,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©+w*C©+700;return[o*C©-w*S©,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;iz)m[q]=z,a.fillStyle="rgb("+~(s[3]*h)+","+~(s[4]*h)+","+~(s[3]*s[3]*-80)+")",a.fillRect(x,y,1,1)}',0)
// end of submission //

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js