Я недавно пересматривал старые книги на днях. В книге Commodore Step by Step Graphics Vol 3 я наткнулся на интересно выглядящий скриншот с небольшим по размеру кодом. На нём была показана затенённая сфера полностью сделанная из точек.

Я подумал, что это будет выглядеть круто, если нарисовать их поверх другу друга, чтобы они выглядели как планеты и луны. Поэтому я перевёл Basic код в JavaScript и использовал canvas для воспроизведения эффекта.

Вот как выглядит результат:
3D планеты из точек на canvas

И так, как достичь подобного эффекта. Вот функция для создания готовых планет:

function drawPlanet(rad, xc, yc, color) {
  ctx.fillStyle = "#000";
  ctx.beginPath();
  ctx.arc(xc, yc, rad, 0, Math.PI * 2);
  ctx.fill();
  ctx.closePath();

  ctx.fillStyle = "rgb(" + color.r + "," + color.g + "," + color.b + ")";
  var x1 = parseInt(Math.sqrt(rad * rad - y * y));
  for (var x = -x1; x < x1; x++) {
    var n = parseInt(Math.random() * x1) / 1.5;

    if (n > x1 + x) {
      ctx.fillRect(x + xc, y + yc, 1, 1);
    }
  }
}

Функция drawPlaten получает четыре параметра:

  • Радиус
  • Цвет планеты (RGB по каналам),
  • Координат центра -- x
  • Координат центра -- y
ctx.beginPath();
ctx.arc(xc, yc, rad, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();

В начале мы рисуем сплошной круг для достижения правильного эффекта перекрытия, в противном случае планеты неправильно перекрывали друг друга. Можете посмотреть на скриншот выше и представить, что точки красной планеты были на жёлтой справа.

Цикле от минус радианы до радианы - это место где и происходит вся магия. На каждой итерации цикла, когда мы идём по кругу, мы вычисляем как далеко строка от центра в x и y до края круга.

У нас есть две точки: радиус (центральная точка) и Y. Таким образом, используя теорему Пифагора ({{< tex >}}a^2 + b^2 = c^2{{< /tex >}}). Мы знаем a, c и можем найти b.

Теорема Пифагора для вычисления края круга

Посмотреть на код, как это работает.

Мы имеем точку, которая находится на краях круга, от (radius, radius+y). Сейчас мы можем проитерироваться от -x до x и получить каждый пиксель между краями круга, на этом строки.

Теперь, когда у нас есть ширина строки, на которой мы находимся, мы можем перебрать каждый пиксель. Следующий шаг, определить какие пиксели надо отросовать, а какие нет. Это делается дальше по коду функции drawPlanet.

for (var x = -x1; x < x1; x++) {
  var n = parseInt(Math.random() * x1) / 1.5;
  if (n > x1 + x) {
    ctx.fillRect(x + xc, y + yc, 1, 1);
  }
}

Переменная n хранит случайное значение между 0 и x1. Чтобы определить нужно ли нам нарисовать точку, мы проверяем n больше x1 + x. Точка будет нарисована только если n > x +x1, что означает x преодолел барьер. Ближе к концу цикла вероятность нарисовать точку увеличивается. Если больше ничего не добавлять, получим прикольный эффект. Пиксели случайно закрашены для половины планеты.

Случайное рисование точек на полусфере

На этом моменте пример из книжки закончился. Однако я хотел добавить больше сферического эффекта. Все что нам нужно - это немного разделить значение.

var n = parseInt(Math.random() * x1) / 1.5;

Я использовал значение 1.5, что уменьшает вероятность нарисовать точку и сдвигает их больше к краю. Так как значение x1 следует за шириной круга, мы получаем крутой эффект сферического затенения.

Ниже примеры затенения, которые можно получить изменяя значение делителя. Варианты затенения планет при разных делителях

И это все, что нам нужно. Довольно простой эффект, использующий немного математики и случайных значений.

See the Pen Planets with points by Loktar (@loktar00) on CodePen.


Новые слова:

  • entirely -- полностью
  • exactly -- в точку
  • solid -- сплошной, твёрдый
  • otherwise -- иначе, в противном случае
  • able -- способны, в состоянии

https://github.com/grishy/blog/blob/hugo/content/post/3d-shading-with-points.md