🦔

GASを使ってVTuberの立ち絵を動かすCSSを効率よく生成する

2021/12/26に公開

この記事は Engineering for Vtuber Advent Calendar 2021 25日目の記事です。

https://qiita.com/advent-calendar/2021/vtuber

針山ハリネとは

お疲れさまです!
バーチャルプログラマーの針山ハリネです。
普段はスマホアプリ開発を中心としたフリーランスエンジニアをやってます。

https://hariyama-harine.web.app/

概要

複数人のVTuberでコラボをするときに、「立ち絵をどうするか」という問題があります。参加者全員が立ち絵をスムーズに動かせるとは限らないので、一番お手軽な「Discordのプラグインを使って、声に合わせて立ち絵を動かす」方法があります。

詳細設定は以下のリンクをご覧ください。

https://sazano123.com/ゲームインスト/5339.html

これを複数人で行う際に、「全員が同じ設定のCSSを簡単に生成できるようにしたい」という気持ちがありました。そこで、スプレッドシートとGASを使って、お手軽に立ち絵を動かすCSS生成シートを作りました。

GAS(Google Apps Script)とは

Googleのサービス上で動かすことができるJavaScriptをベースにしたプログラミング言語です。
GoogleスプレッドシートやGoogleフォームと連携することで、特定の条件で自動的に処理(スプレッドシートに更新があったら通知, フォームに入力があれば自動でSlack通知を送る等)を実行することができます。

Google Apps Script
https://developers.google.com/apps-script

Googleサービスと連携させずにGASだけで動かすことも可能です。また、実行スケジュールを設定することで定期的にGASを実行させることもできます。

設定方法

設定方法はとても簡単でGoogleスプレッドシートを作成後に、拡張機能から「Apps Script」を選択するとGASを利用することができます。

https://docs.google.com/spreadsheets/create

エディタもWebブラウザ上でコーディング・実行できるので開発環境を用意する必要はありません。

一応、claspを導入すれば作成したスクリプトをGitでバージョン管理することもできるようです。

https://github.com/google/clasp

今回のプログラムの内容

ざっくりこのプログラムで行ったことは以下の通りです。

  1. スプレッドシートに「立ち絵CSS生成」のメニューを追加する
  2. 「立ち絵CSS生成」メニューに「キャラ名・画像URL・DiscordID」が設定されたキャラについてCSSを生成するボタンを作る
  3. CSSを生成してダイアログ内のテキストエリアまたはコピーボタンからコピーできるようにする

実際に作成したコード

main.gs
/**
 * @param {number} id
 * @return {void}
 */
const createStyleSheet = (id) => {
  const dlHtml = HtmlService.createTemplateFromFile("dl_dialog");
  dlHtml.css = getCharaCss(id);
  SpreadsheetApp.getUi().showModalDialog(dlHtml.evaluate(), "CSS生成!");
}

/**
 * @param {number} id
 * @return {string}
 */
const getCharaCss = (id) => {
  const data = getData();
  const chara = data[id];
  return css(chara["DiscordID"], chara["キャラ名"], chara["画像URL"]);
}

// スプシからデータ取得
const getData = () => {
  const sheet = SpreadsheetApp.getActiveSheet();

  const lastRow = sheet.getLastRow();
  const lastColumn = sheet.getLastColumn();

  const keys = [];
  const data = [];

  for (let x = 1; x <= lastColumn; x++) {
    keys.push(sheet.getRange(1, x).getValue());
  }

  for (let y = 2; y <= lastRow; y++) {
    const obj = {};
    for (let x = 1; x <= lastColumn; x++) {
      obj[keys[x-1]] = sheet.getRange(y, x).getValue();
    }
    data.push(obj);
  }

  return data;
}

// addMenuでは引数の定義ができないので、ここで個別に関数を作る
const createStyleSheet0 = () => createStyleSheet(0);
const createStyleSheet1 = () => createStyleSheet(1);
const createStyleSheet2 = () => createStyleSheet(2);
const createStyleSheet3 = () => createStyleSheet(3);
const createStyleSheet4 = () => createStyleSheet(4);
const createStyleSheet5 = () => createStyleSheet(5);
const createStyleSheet6 = () => createStyleSheet(6);
const createStyleSheet7 = () => createStyleSheet(7);
const createStyleSheet8 = () => createStyleSheet(8);
const createStyleSheet9 = () => createStyleSheet(9);

const onOpen = () => {
  const ui = SpreadsheetApp.getUi();
  const menu = ui.createMenu("立ち絵CSS生成");
  const charaList = getData();
  menu.addItem("再読み込み","onOpen");
  menu.addSeparator();
  charaList.forEach((chara, index) => {
    menu.addItem(`${chara["キャラ名"]}のCSS生成`,`createStyleSheet${index}`);
  });
  menu.addToUi();
}
dialog.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <title>CSS生成</title>
  </head>
  <body>
    <textarea cols="65" rows="12" name="css" id="css" readonly><?= css ?></textarea>
    <button onClick="copy()">コピー</button>
    <script>
      const copy = () => {
        const textarea = document.getElementById("css");
        textarea.select();
        document.execCommand("copy");
        alert("コピーしました。");
      }
    </script>
  </body>
</html>
base.css.gs
/**
 * @param {string} id
 * @param {string} name
 * @param {string} image
 * @return {string}
 */
const css = (id, name, image) => {
return `
/* ${name}以外の画像を非表示にする */
li.voice-state:not([data-reactid*="${id}"]) { display:none; }

/* ${name}を立ち絵画像に差し替える */
.avatar {
  content:url("${image}");
  height:auto !important;
  width:95% !important;
  border-radius:0 !important;
}

/* 発話状態の設定 */
.speaking {
  border-color:rgba(0,0,0,0) !important;
  position:relative;
  animation-name: speak-now;
  animation-duration: 300ms;
  animation-fill-mode:forwards;
}

@keyframes speak-now {
  0% { bottom:0px; }
  50% { bottom:10px; }
  100% { bottom:0px; }
}

/* ネームタグ表示位置調整 */
li.voice-state{ position: static; text-align: center;}
div.user{ position: absolute; right: 0; left: 0; bottom: 10px; margin: auto; }

/* 色々消すヤツ */
body { background-color: rgba(0, 0, 0, 0); overflow: hidden; }
`
}

base.css.gs の中身は参考リンクのCSSジェネレータを参考に好きなように改造したものを使ってください。

https://manten-do.net/contents/dsk01

作成したスプシ

スプレッドシートの設定も共有するので、良かったら参考にしてください!

https://docs.google.com/spreadsheets/d/1Le681Eota52RcERLoH7-LjT7FhiRsCnh-jL8mLgmtDo/edit#gid=0

実際の動き

https://youtu.be/OwSFYm7q8qg

最後に

最近、お仕事の方が忙しくて動画更新できてないですが、Twitterは頻繁に見ているので、似たような人がいたらぜひ色々教えて下さい!🙏

https://twitter.com/hariyama_harine

YouTubeのvideoIDが不正ですhttps://www.youtube.com/channel/UChGZIEQm8Jlub-1-Xrn45Wg

ここのDiscordにもよく滞在してます!

https://twitter.com/OkuriSae/status/1451307827759222787

Discussion