虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

壊滅的に絵を書くことが苦手でもなんとかしてくれる!p5.js × ChatGPTで クリエイティブコーディング!

こんにちは。虎の穴ラボの H.K です。
本記事は2023 夏のブログ連載企画19日目の記事になります。 明日はA.Mさんのブログが公開されます。ご期待ください!
今週は「見た目でわかるビジュアルネタ記事」というテーマです!

このテーマでブログを書くことにしたのですが、私は絵を描くことが得意ではありません。ただ、メンバーへの説明資料等でどうしても「それっぽい」イラストがほしいケースがあります。
AI 技術が発達した現代、きっと技術がこんな私も救ってくれるはずです。
というわけで今回はp5.jsChatGPTを利用してクリエイティブコーディングにチャレンジしてみます。

クリエイティブコーディングとはなにか

せっかくなので、これも ChatGPT に聞いてみます。

クリエイティブコーディングとはなんですか?

回答

クリエイティブコーディングは、プログラミングと創造性を組み合わせたアプローチです。従来のプログラミングは主に機能やロジックを実現するために使用されますが、クリエイティブコーディングでは、コードを使って芸術的な表現やインタラクティブな体験を創造することを目指します。

クリエイティブコーディングは、プログラミングの技術を用いてアート、デザイン、音楽、映像、インスタレーションなどのクリエイティブなプロジェクトを実現するために利用されます。プログラムを通じてアイデアや表現を具現化し、視覚的なエフェクトやインタラクティブな要素を組み込むことで、新たな形態の表現や体験を生み出すことができます。

クリエイティブコーディングでは、プログラミング言語やツール、フレームワークなどを使って実現されます。例えば、JavaScriptやProcessing、openFrameworks、Unityなどが一般的に使用される言語やツールです。

クリエイティブコーディングは、プログラミングを学ぶだけでなく、創造力や表現力を活かすためにも重要です。また、アーティストやデザイナー、クリエイターにとっては、自身のアイデアを具現化し、インタラクティブな作品を制作するための強力なツールとなります。

長い!!

というわけで再質問します。

もっと簡潔に教えてください。

回答

クリエイティブコーディングは、プログラミングを使って芸術的な作品やインタラクティブな体験を作り出すアプローチです。プログラミング言語やツールを使って、新しい形態の表現や視覚的なエフェクトを実現します。アーティストやデザイナーにとっては、創造力を活かすための強力なツールとなります。

重要なところだけ抜き出すと次の一文になります。

プログラミングを使って芸術的な作品やインタラクティブな体験を作り出すアプローチです。

p5.js とはなにか

続いては p5.js です。 これは公式サイトに簡潔に記載されています。

p5js.org

p5.js is a JavaScript library for creative coding, (以降省略)

ざっくりと訳すと「p5.jsはクリエイティブコーディングをするための JavaScript ライブラリです。」とのことで今回の用途にぴったりですね。

なぜp5.jsChatGPTでクリエイティブコーディングを行うのか

今回、p5.jsChatGPTでクリエイティブコーディングをやってみた背景としては 2 つの理由があります。
1 つは冒頭に書いた通り、イラストを描くことが苦手で、このアプローチならある程度、自由にイラストの出力ができるのではないかと考えたからです。 画像生成専用のシステムを利用したほうが高いクオリティーのものが出力されますが、メンバーへの説明や LT 資料での利用が主体なので、描き込みの多さはノイズになるように思えます。
つまり、このクリエイティブコーディングの方法で「よしなに抽象化されたイラスト」の生成を期待しています。
2 つ目の理由としては虎の穴ラボとしても参加した、技術書典 14 で AI にクリエイティブコーディングをさせるというアプローチの本を見つけて興味を持ったからです。
その本はこちらです!

techbookfest.org

▼ 虎の穴ラボの技術書典 14 のオフライン参加レポートはこちら

toranoana-lab.hatenablog.com

ChatGPTと実装する

ひとまずChatGPTの実力を見てみましょう。 次のプロンプトで質問します。

p5.jsで画面に円を表示したいと思います。最小限の構成でhtmlファイルを作成してください

回答のコード部分のみがこちらです。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
    <title>p5.js Circle</title>
    <style>
      body {
        padding: 0;
        margin: 0;
      }
      canvas {
        display: block;
      }
    </style>
  </head>
  <body>
    <script>
      function setup() {
        createCanvas(400, 400); // 画面のサイズを指定
      }

      function draw() {
        background(220); // 背景色を指定
        ellipse(200, 200, 100, 100); // 円を描画
      }
    </script>
  </body>
</html>

出力結果はこのような形です。 この内容なら問題なく進めていけそうですね。

ちなみにコード以外に、以下の内容を添えて回答してくれました。

  • index.htmlというファイル名で保存してWebブラウザで開くと表示できる
  • ellipse関数の引数をいじると円の場所、大きさを調整できる

夜空を描画する

初歩的なことはできることがわかりました。 次はもう少しイラストっぽいものを生成してもらうため、次のプロンプトを渡しました。

夜空に星と半月が浮かぶイラストをp5.jsで作成してください

Script タグ内のみ示します。

function setup() {
  createCanvas(800, 400); // 画面のサイズを指定
  background(0); // 背景色を黒に指定
  noLoop(); // draw()関数を自動的に繰り返し実行しないように設定
}

function draw() {
  drawStars(); // 星を描画
  drawMoon(width - 100, 100, 80); // 半月を描画
}

function drawStars() {
  for (let i = 0; i < 100; i++) {
    let x = random(width); // x座標をランダムに決定
    let y = random(height); // y座標をランダムに決定
    let size = random(1, 4); // 星の大きさをランダムに決定
    noStroke(); // 線を描画しないように設定
    fill(255); // 白色で塗りつぶす
    ellipse(x, y, size, size); // 星を描画
  }
}
function drawMoon(x, y, size) {
  noStroke(); // 線を描画しないように設定
  fill(255); // 白色で塗りつぶす
  ellipse(x, y, size, size); // 円(半月の土台)を描画

  fill(0); // 黒色で塗りつぶす
  arc(x, y, size, size, PI, TWO_PI); // 円弧(半月のくぼみ)を描画
}

実行した結果はこちら。 ちょっと惜しいですね。 次の2点を1つずつ修正してもらいます。

  1. 月の色を黄色にする
  2. 半月を左右で影と月が見える形にする
月の色は黄色にしたいです。

JavaScript のコードは省略して、説明部分だけ示します。

半月の色を黄色に変更するには、drawMoon()関数のfill(255)の呼び出しをfill(255, 204, 0)に変更します。これにより、RGB値(255, 204, 0)の黄色で塗りつぶされます。

実行結果 これで色は想定したものになりました。 ただ単に黄色としか指定していないのにRGB値(255, 255, 0)ではなくRGB値(255, 204, 0)で少しオレンジがかった黄色を指定しているのは賢いですね。

次に私の思い描く半月は「上下に半分になった月」ではなく、「左右に半分になった月」であるため、この件も修正を求めてみます。

半月は右半分に月があり、左半分が影になるように描写してください。

回答

function drawHalfMoon(x, y, size) {
  noStroke(); // 線を描画しないように設定
  fill(255, 204, 0); // 黄色で塗りつぶす

  // 右半分の月を描画
  arc(x, y, size, size, -PI / 2, PI / 2); // 円弧(半月のくぼみ)を描画

  // 左半分の影を描画
  fill(0); // 黒色で塗りつぶす
  arc(x - size, y, size, size, PI / 2, -PI / 2); // 円弧(半月のくぼみの影)を描画
}

実行結果 ちなみに上記のプロンプトの前に以下のプロンプトを与えたところ、「上下に半分になった月」のコードを再度提示してきましたので、やって欲しいことを明確に伝えたほうが良さそうですね。

失敗したプロンプト

半月を描画するコードは上下で半分になっているようでした。

三日月にする

『半分の月がのぼる空』はとてもきれいなのですが、イラストだと三日月をよく見る気がします。
そこで、今回の月も三日月にしてもらおうと思います。

そのコードを元に月を三日月にしてください。
function drawCrescentMoon(x, y, size) {
  noStroke(); // 線を描画しないように設定
  fill(255, 204, 0); // 黄色で塗りつぶす

  // 右半分の三日月の土台を描画
  arc(x, y, size, size, -PI / 2, PI / 2); // 円弧(三日月のくぼみ)を描画

  // 左半分の三日月の土台を描画
  arc(x - size, y, size, size, PI / 2, -PI / 2); // 円弧(三日月のくぼみ)を描画

  // 左半分の三日月の土台を黒く塗りつぶすことで影を表現
  fill(0); // 黒色で塗りつぶす
  arc(x - size, y, size, size, -PI / 2, PI / 2); // 円弧(三日月のくぼみの影)を描画
}

実行結果 \\ぱっかーん//
うーん……。何度か試してみましたが難しそうです。
ここまできたら自分でやってしまいましょう!

function drawCrescentMoon(x, y, size) {
  noStroke(); // 線を描画しないように設定
  fill(255, 204, 0); // 黄色で塗りつぶす
  ellipse(x, y, size, size); // 円(半月の土台)を描画

  fill(0); // 黒色で塗りつぶす
  arc(x - 20, y - 20, size, size, 0, TWO_PI); // 円弧(三日月のくぼみ)を描画
}

きれいに描画できていますね!

うまくいかなかった例

今回、夜空と月を描いてもらいましたが他にもいくつか試してみましたので、うまくいかなかったパターンを紹介します。

フラクタル図形の描画

図形を書かせる上でちょっと複雑なものとなるとフラクタル図形が有名かなと思います(要出典)。
フラクタル図形とは「図形の部分と全体が自己相似(再帰)になっているものなどをいう」とのことです。

ja.wikipedia.org

今回試したのは以下の3つの構造、曲線で、ドラゴン曲線の描画はうまくいきませんでした。

  • シェルピンスキーのギャスケット構造
  • コッホ曲線
  • ドラゴン曲線

シェルピンスキーのギャスケット構造

この構造は某ハイラルの勇者様にはおなじみの三角形が無数に重なった構造を示します。

function setup() {
  createCanvas(800, 800); // 画面のサイズを指定
  background(255); // 背景色を白に指定
  noStroke(); // 線を描画しないように設定

  // ギャスケットの初期三角形の頂点座標を指定
  const triangle = [
    createVector(400, 100),
    createVector(100, 700),
    createVector(700, 700),
  ];

  drawSierpinskiGasket(triangle, 4); // ギャスケットを描画
}

function drawSierpinskiGasket(triangle, depth) {
  if (depth === 0) {
    // 再帰の終了条件: 最下層まで到達したら三角形を描画
    drawTriangle(triangle);
  } else {
    // 再帰の処理: 三角形を3つの小さな三角形に分割し、各々を再帰的に描画
    const v1 = triangle[0].copy();
    const v2 = triangle[1].copy();
    const v3 = triangle[2].copy();

    const mid1 = p5.Vector.lerp(v1, v2, 0.5);
    const mid2 = p5.Vector.lerp(v2, v3, 0.5);
    const mid3 = p5.Vector.lerp(v3, v1, 0.5);

    // 3つの小さな三角形を再帰的に描画
    drawSierpinskiGasket([v1, mid1, mid3], depth - 1);
    drawSierpinskiGasket([mid1, v2, mid2], depth - 1);
    drawSierpinskiGasket([mid3, mid2, v3], depth - 1);
  }
}

function drawTriangle(vertices) {
  fill(random(255), random(255), random(255)); // ランダムな色で塗りつぶす
  triangle(
    vertices[0].x,
    vertices[0].y,
    vertices[1].x,
    vertices[1].y,
    vertices[2].x,
    vertices[2].y
  ); // 三角形を描画
}

実行結果がこちら 実装も簡潔で余計なところがなく、綺麗なコーディングだなという印象を受けます。

コッホ曲線

コッホ曲線について以下のように実装してくれました。

const START = createVector(50, 200); // 起点の座標
const END = createVector(750, 200); // 終点の座標
const RECURSION_LEVEL = 4; // 再帰の階層数

function setup() {
  createCanvas(800, 400); // 画面のサイズを指定
  background(255); // 背景色を白に指定
  stroke(0); // 線の色を黒に指定
  noLoop(); // draw()関数を自動的に繰り返し実行しないように設定

  drawKochCurve(START, END, RECURSION_LEVEL); // コッホ曲線を描画
}

function drawKochCurve(start, end, level) {
  if (level === 0) {
    line(start.x, start.y, end.x, end.y); // 線分を描画
  } else {
    const a = p5.Vector.sub(end, start).div(3); // 線分の1/3地点
    const p1 = p5.Vector.add(start, a); // 線分の1/3地点
    const p3 = p5.Vector.sub(end, a); // 線分の2/3地点
    const b = p5.Vector.sub(p3, p1).rotate(-radians(60)); // 60度回転したベクトル
    const p2 = p5.Vector.add(p1, b); // 線分の中央に伸ばした部分の座標

    drawKochCurve(start, p1, level - 1); // 再帰的に左側の線分を描画
    drawKochCurve(p1, p2, level - 1); // 再帰的に中央の線分を描画
    drawKochCurve(p2, p3, level - 1); // 再帰的に右側の線分を描画
    drawKochCurve(p3, end, level - 1); // 再帰的に右端の線分を描画
  }
}

このまま実行するとcreateVectorの関数が見つからずエラーになるので、以下のように修正を加えて対応しました。

// const START = createVector(50, 200); // 起点の座標
// const END = createVector(750, 200); // 終点の座標

function setup() {
  createCanvas(800, 400); // 画面のサイズを指定
  background(255); // 背景色を白に指定
  stroke(0); // 線の色を黒に指定
  noLoop(); // draw()関数を自動的に繰り返し実行しないように設定

  drawKochCurve(createVector(50, 200), createVector(750, 200), RECURSION_LEVEL); // コッホ曲線を描画
}

実行結果がこちらです。

この調子でドラゴン曲線も、と思ったのですが、そうはいきませんでした。

ドラゴン曲線

ChatGPTが提案したドラゴン曲線のコードは次の通りです。

// const START = createVector(100, 200); // 起点の座標
// const END = createVector(700, 200); // 終点の座標
const RECURSION_LEVEL = 4; // 再帰の階層数
// const INITIAL_DIRECTION = createVector(1, 0); // 初期の向き

function setup() {
  createCanvas(800, 400); // 画面のサイズを指定
  background(255); // 背景色を白に指定
  stroke(0); // 線の色を黒に指定
  noLoop(); // draw()関数を自動的に繰り返し実行しないように設定

  drawDragonCurve(
    createVector(100, 200),
    createVector(700, 200),
    RECURSION_LEVEL,
    createVector(1, 0)
  ); // ドラゴン曲線を描画
}

function drawDragonCurve(start, end, level, direction) {
  if (level === 0) {
    line(start.x, start.y, end.x, end.y); // 線分を描画
  } else {
    const a = p5.Vector.sub(end, start).div(2); // 線分の中心点
    const b = direction.copy().rotate(-HALF_PI); // 現在の向きを90度回転したベクトル

    const p1 = p5.Vector.add(start, a); // 線分の中心点
    const p2 = p5.Vector.add(p1, b); // 線分の中心点から回転したベクトル分だけ伸ばした座標

    drawDragonCurve(start, p1, level - 1, direction.copy()); // 再帰的に左側の線分を描画
    drawDragonCurve(end, p1, level - 1, direction.copy().rotate(PI)); // 再帰的に右側の線分を描画
    drawDragonCurve(p1, p2, level - 1, direction.copy()); // 再帰的に曲がった線分を描画
  }
}

表示してみると以下のノイズのような線が描画されました。

もう少し詳細に「ヘイウェイのドラゴン」を指定してみましたが、同じようにノイズのような線になりました。
以下がコードです。

// const START = createVector(100, 200); // 起点の座標
// const END = createVector(700, 200); // 終点の座標
const RECURSION_LEVEL = 12; // 再帰の階層数
// const INITIAL_DIRECTION = createVector(1, 0); // 初期の向き

function setup() {
  createCanvas(800, 400); // 画面のサイズを指定
  background(255); // 背景色を白に指定
  stroke(0); // 線の色を黒に指定
  noLoop(); // draw()関数を自動的に繰り返し実行しないように設定

  drawHeighwayDragonCurve(
    createVector(100, 200),
    createVector(700, 200),
    RECURSION_LEVEL,
    createVector(20, 0)
  ); // ヘイウェイのドラゴン曲線を描画
}

function drawHeighwayDragonCurve(start, end, level, direction) {
  if (level === 0) {
    line(start.x, start.y, end.x, end.y); // 線分を描画
  } else {
    const a = p5.Vector.sub(end, start).div(2); // 線分の中心点
    const b = direction.copy().rotate(-HALF_PI); // 現在の向きを90度回転したベクトル

    const p1 = p5.Vector.add(start, a); // 線分の中心点
    const p2 = p5.Vector.sub(p1, b); // 線分の中心点から回転したベクトル分だけ伸ばした座標

    drawHeighwayDragonCurve(
      start,
      p2,
      level - 1,
      direction.copy().rotate(-HALF_PI)
    ); // 再帰的に左側の線分を描画
    drawHeighwayDragonCurve(
      end,
      p2,
      level - 1,
      direction.copy().rotate(HALF_PI)
    ); // 再帰的に右側の線分を描画
  }
}

createVector(1, 0)の定義がよくなさそうなので少しいじってみます。
createVector(20, 0)にして実行した結果がこちら。さっきより意図が見えてくる表示になりましたが、ここから正しい実装に修正していくのは微修正では済まなくなりそうです。

私がもう少し実装に詳しくてStep by Stepで実装していければ、生成できそうな感じも受けましたが、一発で生成というのはできませんでした。

まとめ

ChatGPTを使えばクリエイティブコーディングもイメージを伝えるだけで画像が生成できることがわかりました。
もっとサービスで使えそうなイラストを生成させたり、p5.jsではなくPlantUMLを経由してUMLも作ってもらったりと、DSLを生成させる目的で使うのにもとても便利だと認識しました。
Tone.jsでWebページのBGMを作っても面白いかもしれませんね。

プロンプト作成時の感覚的なTips

あくまで主観ですが、次の項目に気をつけると想定した通りの回答が返ってくることが多いと感じました。

  • やってもらいたいことを明確に指示する
    • NG例:「〇〇は違います。」のみ
    • OK例:「〇〇は違います。△△のように修正してください。」
  • 目的と手段の両方を示す

巷でもよく言われていることですが、人に教える、質問する場合と同様、背景含めて丁寧にプロンプトを作成したほうが良さそうです。

今回初めてクリエイティブコーディングに挑戦してみましたが、自分でもきれいな図形やイラストを実装してみたいと感じました。
その際もChatGPTさんには、ご是非協力いただきたいです!

採用情報

虎の穴では一緒に働く仲間を募集中です!

この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。

カジュアル面談やエンジニア向けイベントも随時開催中です。ぜひチェックしてみてください。 yumenosora.co.jp