2010-01-27

NoCanvas Introduction

3D с z-buffer-ом, субпиксельной точностью и освещением по Гуро на javascript? Да кто угодно сможет это сделать, используя canvas!

Можно долго и вкусно описывать преимущества канваса, но статья не про это; не менее интересно
посмотреть, чем же канвас плох.

Так чем же?

  • Он ни в какую не работает в Обозревателе Интернета одной большой известной корпорации: эмулируя канвас через VML мы отрезаем самые вкусные пиксельные манипуцляции, а эмуяция пиксельных вкусняшек на flash безбожно тормозит из-за того, что где-то в недрах ExternalInterface разработчики из Adobe вставили пару функций sleep :)
  • Только Google Chrome (aka Chromium) может обеспечить достаточную скорость исполнения джаваскрипта. Во всех остальных браузерах вкусный и сексуальный эффект рискует превратиться в слайд-шоу.
  • И, наконец, самое главное — это неспортивно! Я уверен, что пока есть люди пишущие сапёров на bat файлах, тетрисы на sed и боярские диалекты C++, программирование ради самого процесса программирования будет интересовать массы :)
Я ненормальный псих завёл блог, в котором собираюсь регулярно писать о создании различных эффектов и игр на javascript, не использующих канвас:
  • раз в две недели я буду писать о каком-либо новом эффекте;
  • раз в два месяца я буду рассказывать делать 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>&lt;canvas&gt; 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();
}
Эпилог
Полный текст скрипта доступен по адресу http://nocanvas.zame-dev.org/0000/index.html (просто загляните в исходный код страницы)

1 comment:

  1. Извините, а можете пояснить подробнее, зачем эти кубы создаются или подскажите, где почитать

    ReplyDelete