3D с z-buffer-ом, субпиксельной точностью и освещением по Гуро на javascript? Да кто угодно сможет это сделать, используя canvas!Можно долго и вкусно описывать преимущества канваса, но статья не про это; не менее интересно
посмотреть, чем же канвас плох.
Так чем же?
- Он ни в какую не работает в Обозревателе Интернета одной большой известной корпорации: эмулируя канвас через VML мы отрезаем самые вкусные пиксельные манипуцляции, а эмуяция пиксельных вкусняшек на flash безбожно тормозит из-за того, что где-то в недрах ExternalInterface разработчики из Adobe вставили пару функций sleep :)
- Только Google Chrome (aka Chromium) может обеспечить достаточную скорость исполнения джаваскрипта. Во всех остальных браузерах вкусный и сексуальный эффект рискует превратиться в слайд-шоу.
- И, наконец, самое главное — это неспортивно! Я уверен, что пока есть люди пишущие сапёров на bat файлах, тетрисы на sed и боярские диалекты C++, программирование ради самого процесса программирования будет интересовать массы :)
- раз в две недели я буду писать о каком-либо новом эффекте;
- раз в два месяца я буду рассказывать делать playable demo какой-нибудь игры (как водится, не использующую canvas);
- и, наконец, раз в полгода я буду делать по игре (ну, по крайней мере, я буду очень стараться не сорвать сроки)
Небольшая еретическая статья на затравку — как сделать 3D + z-buffer + subpixel + gouraud shading используя канвас
Шаг 0
Для начала, необходимо позаботится о пользователях IE, предложив им поддержку тэга canvas в виде Chrome Frame: прописываем meta, подключаем гуглоскрипт, создаём блок no-canvas и правим стили для гугловского iframe (по умолчанию он появляется в центре страницы)<head>
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<style type="text/css" media="screen">
.chrome-install {
position: relative;
margin: 0;
padding: 0;
top: 0;
left: 0;
}
</style>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
</head>
<body onload="main()">
<div id="no-canvas" style="display:none;">
<h2><canvas> support required.</h2>
<div id="chrome-install-placeholder"></div>
</div>
<div id="canvas-enabled">
<canvas id="canvas" width="384" height="384"></canvas>
</div>
</body>
Функция main (init вызывается по таймауту, потому что иначе хром (dev версия) иногда не прорисовывает background у body до конца):function main()
{
canvas = document.getElementById('canvas');
if (typeof(canvas.getContext) == 'function')
{
ctx = canvas.getContext('2d');
setTimeout(init, 100);
}
else
{
document.getElementById('canvas-enabled').style.display = 'none';
document.getElementById('no-canvas').style.display = '';
CFInstall.check({
node: 'chrome-install-placeholder',
className: 'chrome-install',
destination: window.location.href
});
}
}
Шаг 1
Всю отрисовку будем вести в буфере 128x128, а потом его выводить с 3-х кратным увеличением (используя putImageData).var scr = [];
var zbuf = [];
var WDT = 128;
var HGT = 128;
function blit()
{
var cd = ctx.createImageData(canvas.width, canvas.height);
var data = cd.data;
var ind = 0;
for (var y = 0; y < HGT; y++)
{
var line = scr[y];
for (var x = 0; x < WDT; x++)
{
data[ind++] = line[x][0]; data[ind++] = line[x][1]; data[ind++] = line[x][2]; data[ind++] = 255;
....
}
ctx.putImageData(cd, 0, 0);
}
Шаг 2
Представим фигуру в виде объекта и напишем читерскую функцию box, которая будет создавать кубы сразу с нормалями:{
points: [
{x: point_x, y: point_y, z: point_z, n: { x: point_normal_x, y: point_normal_y, z: point_normal_z } },
....
],
faces: [
[point_1, point_2, point_3, { x: face_normal_x, y: face_normal_y, z: face_normal_z }],
....
],
color: object_color,
rot: [rot_x, rot_y, rot_z]
}
function box(size, cl, rot)
{
var norm = 1 / Math.sqrt(3);
return {
points: [
{ x: -size, y: size, z: -size, n: { x: -norm, y: norm, z: -norm } },
....
],
faces: [
[ 0, 1, 2, { x: 0, y: 1, z: 0 } ],
....
],
color: cl,
rot: rot
};
}
Шаг 3
Выполним всю чёрную работу — повернём объект (вместе с нормалями — это быстрее, чем считать нормали заново) и спроецируем его в 2D (мне было лень нормально считать уровень освещённости, по-этому я использовал метод научного тыкаtm для нахождения магической формулы и магических коеффициентов 0.7 и 3)function project(pt, rm)
{
var rot = {
x: (pt.x*rm.oxx + pt.y*rm.oxy + pt.z*rm.oxz),
y: (pt.x*rm.oyx + pt.y*rm.oyy + pt.z*rm.oyz),
z: (pt.x*rm.ozx + pt.y*rm.ozy + pt.z*rm.ozz + ZPOS)
};
var l = 1 - (Math.cos(pt.n.x*rm.ozx + pt.n.y*rm.ozy + pt.n.z*rm.ozz) - 0.7) * 3;
l = Math.max(0, Math.min(1, l));
return {
x: ((WDT / 2) + rot.x * (WDT / 2 - 1) / rot.z),
y: ((HGT / 2) + rot.y * (HGT / 2 - 1) / rot.z),
z: rot.z,
l: l
};
}
function draw_object(obj)
{
var ax = (tm * obj.rot[0]);
var ay = (tm * obj.rot[1]);
var az = (tm * obj.rot[2]);
var s1 = Math.sin(ax); var s2 = Math.sin(ay); var s3 = Math.sin(az);
var c1 = Math.cos(ax); var c2 = Math.cos(ay); var c3 = Math.cos(az);
var rm = {
oxx: (c2 * c1),
oxy: (c2 * s1),
....
};
var pr = [];
for (var i = 0; i < obj.points.length; i++) {
pr.push(project(obj.points[i], rm));
}
for (var i = 0; i < obj.faces.length; i++)
{
var face = obj.faces[i];
var fz = (face[3].x*rm.ozx + face[3].y*rm.ozy + face[3].z*rm.ozz);
if (fz <= 0) {
triangle(pr[face[0]], pr[face[1]], pr[face[2]], obj.color);
}
}
}
Шаг 4
Напишем процедуру рисования горизонтальных линий (не самую оптимальную) и треугольников (тоже не чемпион по скорости):function hline(y, xl, xr, cl, ll, lr, zl, zr)
{
// Растянем линию горизонтально — так как мы используем треугольники,
// а не честные полигоны, то при субпиксельной отрисовке без этого хака
// иногда проявляются артефакты
xl -= 0.5;
xr += 0.5;
....
}
// **в реальном коде используется изменённая функция**
function triangle(a, b, c, cl)
{
.... сортировка вершин ....
var dxl = (c.x - a.x) / (c.y - a.y);
var dxr = (b.x - a.x) / (b.y - a.y);
....
var y = a.y;
while (y < b.y)
{
// (y - a.y) - не обязательно целочисленное
// за счёт этого достигается субпиксельная точность
xl = sx + dxl * (y - a.y);
xr = sx + dxr * (y - a.y);
....
hline(y, xl, xr, cl, ll, lr, zl, zr);
y++;
}
....
}
Шаг 5
Остаётся написать функцию loop и запустить её через setInterval.function loop()
{
tm = ((new Date()).valueOf() - st) / 1.5;
draw_scene();
blit();
}
Извините, а можете пояснить подробнее, зачем эти кубы создаются или подскажите, где почитать
ReplyDelete