見出し画像

文字が流れる壁譜面を作る

こんにちは!レイクンです。

最近BeatSaberの譜面作りを始めたばかりの初心者マッパーです!
いくつか譜面を作ったところで、そろそろ壁譜面も作ってみたいなぁと思い始めました。

壁譜面というと、カラフルで綺麗な模様の図形が流れてきたり、圧倒されるほど巨大な構造物が建築されていたり、というようなものを想像する方が多いでしょう。

しかし、何を思ったのか「文字流したいなぁ」と考えた私は、初の自作壁譜面で真っ先に壁を使って文字を書き始めたのでした...

さて、今回はそんな"壁文字"のインスピレーションを与えてくださった先駆者様の譜面を紹介させていただくとともに、実際にどのように壁文字を作ったのか、またその作業の効率化についてお話させていただきます。

記事の最後では、今回作った壁文字をいつでも使い回せるよう、壁譜面制作支援ツール「Beatwalls」で使える関数を配布します!

※この記事はBeat Saber Advent Calendar 2020の22日目の記事です。​

1. 壁文字を含む譜面たち

壁で文字が書いてある譜面はそこまで数が多いわけではないと思うので、BeatSaberを始めたばかりでそのような譜面をあまり見たことがない方もいらっしゃるかもしれません。そこでまずは、私が感銘を受けた先駆者様の壁文字譜面をいくつか紹介させていただきます。


ECHO - MafuMafu feat.nqrse (Crusher-P)
!bsr: (削除済み) Mapper: barynaoさん

歌詞に合わせて英単語の文字が流れてくる譜面です。壁文字としてはシンプルながら曲の雰囲気にとてもよく合っていてカッコいいです。個人的には壁文字という概念に出会って間もない頃に知ってとても感動した譜面で、思い入れがあります。紹介しておいてアレですが、残念ながら既に削除されてしまっているようです...


S・O・S - Nanahira (cover)
!bsr: 7da4 Mapper: ririkkuさん

サビの「S・O・S」の歌詞に合わせてアルファベットが流れてきます。横長の直方体を積み重ねて作られた独特なスタイルのフォントです。その大きさは非常に存在感があります。曲のキャッチーなフレーズに合わせて壁文字が飛び出してくる演出は、定番ですがインパクトがあって良いですよね。また、文字と言えるかは分かりませんが、イントロ・アウトロ部分では「S・O・S」を表すモールス信号が壁で表現されているのも面白いポイントです。


BB 2020 | PSYQUI feat. Marpril - Girly Cupid | 14 | Submission #41
!bsr: a75c Mapper: Bloodcloakさん

以前行われたマッピングコンテスト「Building Blocks 2020」の提出作品の一つで、Beatwallsを使用した壁譜面のExampleとしてBeatwalls Documentation内でも挙げられているものです。直方体を組み合わせたものと細い線を組み合わせたものの2種類のフォントが登場します。プレイヤーの背後から飛び出す「heart」が印象的です。


Star Wars IV opening crawl (wall art) [NE 1.2]
!bsr: cd09 Mapper:  nyri0さん

これはまさに壁文字が主役の譜面と言って良いでしょう。映画スター・ウォーズのオープニングであらすじの文章が流れていくアレです。シンプルに文字がゆっくりと流れていくだけではありますが、実際にプレイしてみるとその迫力は圧巻です!実はこれらの壁文字、なんとPythonを使って自動的に生成・配置されているものだそうです。


17Sai - Irozuku Sekaino Asukara OP Full
!bsr: 7b7f  Mapper:  Emirさん & Soba'sさん & Rilianさん

無題

この譜面は、3人のマッパーさんによる合作譜面です。壁文字がメインの譜面でないのですが、プレイしていると随所で3人のお名前の壁文字が流れてきます。これは、そのパートのマッピングを担当された方のお名前を表しているそうです。そういう壁文字の使い方もあるんですね。マッパーさんごとのマッピングの特徴を見比べながらプレイしてみるのも面白いですよ!
※動画は消されてしまったので画像のみでのご紹介になりますm(_ _)m
壁文字以外の壁の演出も曲と合っていて素敵な譜面なので是非実際にプレイしてみてください!


Forest Map
!bsr: 7e99 Mapper:  meause

こちらは、歌詞に合わせて日本語の文字が流れて来る譜面です。この譜面はNiMさんという方のお誕生日プレゼントとして製作されたもので、ラストにはお祝いのメッセージが流れてきます。このように壁文字を使って譜面に特別なメッセージを込めることもできるのですね!


2. 見様見真似で作ってみた

壁譜面の制作方法はいくつかありますが、今回はBeatwallsというツールを使いました。

Beatwallsは、壁の構造・タイミング・位置などを直感的に指定できる専用言語によって書かれたスクリプトファイルを、NoodleExtensionsやChromaなどのマッピング拡張modでプレイできる形式の譜面データに変換してくれるツールです。Beatwallsでは、あらかじめいくつかの壁のパターンの関数が用意されており、それらの組み合わせやプロパティ設定によって様々なパターンの壁を簡単に作ることができます。

とはいえ、今回の目的は文字を形造ること。通常の図形や模様と違って対称性がなく、既存パターンをいくつか組み合わせだけでは再現が難しいため、点や線など小さなパーツをたくさん繋げて文字を書いていく必要があります。そのためには、どの位置にどんな大きさの要素を配置するかを決める設計図が必要です。

全ての壁は直方体から成り立っているため、ベースとなる文字フォントも四角形の組み合わせで表現されるもののほうが、設計図を作りやすいと言えます。そこで今回は、ベースフォントとして「ドット明朝 16 Std M」を使用しました。等幅フォントなので大きさを揃えやすいという利点もあります。

画像1

ペイントソフトを使ってベース文字にグリッドを重ね、手作業でパーツの位置と大きさを1つずつ抽出し、Beatwallsで用いられる座標系のスケールに変換、壁文字を生成する関数を作っていきます。

#Beatwalls関数

#「あ」のパーツ1
define: _kanaA1
   structures: Wall
   startRow: -0.25 #x座標
   startHeight: 4 #y座標
   width: 0.25 #幅
   height: 0.5 #高さ

#「あ」のパーツ2
define: _kanaA2
   structures: Wall
   startRow: 0.25
   startHeight: 4
   width: 0.5
   height: 0.25

# ~~~中略~~~

#「あ」を生成する関数
#上で定義したパーツを全て結合する
define: kanaA
   structures: _kanaA1,_kanaA2,_kanaA3,_kanaA4,_kanaA5,_kanaA6,_kanaA7,_kanaA8,_kanaA9,_kanaA10,_kanaA11,_kanaA12,_kanaA13,_kanaA14,_kanaA15,_kanaA16,_kanaA17,_kanaA18,_kanaA19,_kanaA20,_kanaA21

慣れれば1文字あたり10分程度で関数化することができますが、正直地獄のような作業でした...
絶対もっと効率化できるだろうと薄々感づいてはいましたが、このときの譜面はビー祭でお披露目用に作っていたので時間に余裕がなく、虚無になりながらもひたすら手を動かすしかありませんでした...

そうして精神を削りながらも完成した譜面がこちら。

サビの合いの手に合わせて文字が飛び出すという、やりたかった演出を自分のイメージ通りに表現することができたし、ついでに他のいろんな種類の壁も作ることができました。初めて触れるBeatwallsの勉強にもなったし、出来としてはとても満足しています。文字を作るのはかなり大変でしたが...


3. 壁文字制作の効率化

人類は二度とあのような苦行を強いられてはならない...

そんな思いから、壁文字をフォント画像から自動生成するプログラムを作成することにしました。

※ここからはプログラミングの話になるのであまり興味がない方はこの章は飛ばして構いません。次の4章で完成した壁文字の紹介と配布をします。

先に紹介したnyri0さんのスター・ウォーズ壁文字譜面も同様に、フォント画像から壁文字を生成するプログラムが使用されており、ソースコードも公開されていますが、
 ・特定の譜面製作用のコードのみで汎用的なものは公開されていない
 ・英語アルファベットのみで日本語ひらがな・カタカナ等は未対応
 ・Beatwalls関数ではなくjson譜面データ形式に直接変換される
といったところが少し使いづらい....

壁文字以外の壁との併用や使い回しを考えると、特にBeatwalls関数として出力してあげるのは重要な気がします。(個人的には日本語が使いたいというのも大きい)

そこで、日本語文字の壁を作成できるBeatwalls関数を自動生成することを目指し、スクリプトの作成に臨みました。言語はPython、ベースフォントは先ほどと同じく「ドット明朝 16 Std M」を使用しました。

まずは、Beatwalls関数化したい文字フォントの一覧画像を用意します。
今回は「ひらがな」「カタカナ」「英語アルファベット大文字・小文字」「数字・記号」の4つのセットを用意しました。

画像2

画像3

画像4

画像5

いい感じにトリミングして1文字ずつ抜き出していきます。
こういう画像処理はPythonで簡単にできますね。

from PIL import Image
import numpy as np
from setting import * #トリミングサイズとかの定数を別ファイルにまとめています

for k,v in func_name.items():
	im = Image.open('%s.png'%k)
	i = 0
	for r in range(n_row):
		for c in range(n_column):
			left  = (letter_width+letter_space)*c
			upper = (letter_hight+line_space)*r
			right = left+letter_width
			lower = upper+letter_hight
			im_letter = im.crop((left,upper,right,lower))
			if (np.array(im_letter)[:,:,:3].sum(axis=2)-(255*3)).sum() == 0:continue
			im_letter.save('%s_image/%02d_%s.png'%(k,i,v[i]))
			i+=1

キャプチャ

これらの文字は上下左右の余白部分の大きさが1つずつ異なっているため、あとで文字毎に位置がズレてしまわないように左端と下端からのオフセットを記録しておきます。

次にこれらの文字形状を数値データに変換していきます。
各文字の画像を壁の最小要素となる直方体サイズのグリッドで分割し、白い区画(文字ではない部分)は0、黒い区画(文字の部分)は1となるような二値行列に変換します。

#二値行列を取得
def get_binary_matrix(im):
	w,h = im.size
	n_x = round_int(w/dot_width) #横のdot数
	n_y = round_int(h/dot_hight) #縦のdot数
   
	binary_matrix = []
	for iy in range(n_y):
		X = []
		for ix in range(n_x):
			c_x = int(ix*dot_width+(dot_width/2.)) #dot中央のx座標
			c_y = int(iy*dot_hight+(dot_hight/2.)) #dot中央のy座標
			value = np.array(im)[c_y][c_x][0]
			X.append(1 if value<120 else 0)
		binary_matrix.append(X)
	return binary_matrix

名称未設定

左図の二値行列からそのままBeatwalls関数を作成してもよいのですが、正方形を継ぎ接ぎしたような状態になるので、壁にしたときの見栄えが少し悪そうです。右図のように長方形の組み合わせに変換できるとより自然でしょう。

最適な長方形の組み合わせは人が目で見れば一目瞭然ではありますが、プログラムに自動でやらせようとすると一捻り必要です。これを実現するためには「複合長方形領域の最小分割問題」と呼ばれる問題を解くアルゴリズムが必要になるようですが、幸いなことに先駆者nyri0さんは貪欲法で解く関数を実装されていましたので拝借することにしました。

#複合長方形領域の最小分割問題を貪欲法で解く
def div2rectangle(binary_matrix):
	font = []
	for k, matrix in enumerate(binary_matrix):
		matrix = (np.array(matrix))
		char_walls = []
		while True:
			h_lines = []
			v_lines = []
			# Horizontal lines
			for i in range(len(matrix)):
				start_j = None
				for j in range(len(matrix[0])):
					if matrix[i, j] and start_j is None:
						start_j = j
					if not matrix[i, j] and start_j is not None:
						h_lines.append((j - start_j, i, start_j))
						start_j = None
				if start_j is not None:
					h_lines.append((len(matrix[0]) - start_j, i, start_j))
			# Vertical lines
			for j in range(len(matrix[0])):
				start_i = None
				for i in range(len(matrix)):
					if matrix[i, j] and start_i is None:
						start_i = i
					if not matrix[i, j] and start_i is not None:
						v_lines.append((i - start_i, start_i, j))
						start_i = None
				if start_i is not None:
					v_lines.append((len(matrix) - start_i, start_i, j))
			# Choose the longest line, add it and clear the pixels
			h_lines.sort(key=lambda x: x[0], reverse=True)
			v_lines.sort(key=lambda x: x[0], reverse=True)
			if not h_lines and not v_lines:
				break
			elif not h_lines or v_lines[0][0] >= h_lines[0][0]:
				h, i, j = v_lines[0]
				char_walls.append((j, len(matrix) - i - h, 1, h))
				for r in range(i, i + h):
					matrix[r, j] = False
			else:
				w, i, j = h_lines[0]
				char_walls.append((j, len(matrix) - i - 1, w, 1))
				for c in range(j, j + w):
					matrix[i, c] = False
		font.append(char_walls) #(x,y,w,h)
	return font

この関数に通すと、連続して真っ直ぐ連なった正方形同士は結合され、長方形にまとめられます。見た目もそうですが、文字を表すのに使用する壁の数も減るので、ゲームが重くなるのを防ぐ効果もあります。

無事長方形の組み合わせにできたところで、最後にBeatwallsの関数に変換していきます。Wall型のstructureに対してx,yの位置座標と幅,高さの数値を設定し、テキストファイル(拡張子.bw)として出力していきます。

#Beatwalls関数を生成
def make_beatwalls_func(name, font, minimum_box):
	combined_text = ''
	for char, walls, (left,_,__,lower) in zip(name,font,minimum_box):
		x_offset = (left/dot_width-n_dot_x/2)
		y_offset = (letter_hight-lower)/dot_hight
		text = ''
		parts_name = []
		for i, (_x,_y,_w,_h) in enumerate(walls):
			x = (_x+x_offset)*scale
			y = (_y+y_offset)*scale
			w = _w*scale
			h = _h*scale
			name = '_%s_%s_%02d'%(k,char,i)
			parts_name.append(name)
			text += 'define: %s\n\tstructures: Wall\n'%name
			text += '\tstartRow: %f\n\tstartHeight: %f\n\twidth: %f\n\theight: %f\n\n'%(x,y,w,h)
		text += 'define: %s_%s\n\tstructures: %s\n\n'%(k,char, ', '.join(parts_name))
		combined_text += text
		with open('%s_bw/%s_%s.bw'%(k,k,char),'w') as f:
			f.write(text)
	with open('%s.bw'%(k),'w') as f:
		f.write(combined_text)

キャプチャ

こんな感じで1文字毎に関数ファイルが生成されてゆき完成になります。
一応「ひらがな」「カタカナ」「英語アルファベット大文字・小文字」「数字・記号」の各セット毎に全文字をまとめたbwファイルも出力しています。壁文字をたくさん使いたいときはそっちが便利ですね。

4. できたもの紹介

自動生成された壁文字で早速テスト譜面を作ってみました。

AIUEO Song
!bsr: 11dca Mapper: rei05_h

実際にプレイしてみて、想像以上に綺麗に文字を形造れたと思いました。

scaleやadd系のプロパティを変更すれば大きさや位置が変えられます。デフォルトでは、Beatwalls座標系基準でフォントサイズが4×4、位置は文字の中心線がレーン中央に、下側のラインが地面に一致するようになっています。

こんな感じで今回作ったBeatwalls関数を好きなタイミングにセットしていくだけで文字が出現する譜面が完成します。

#Beatwallsでの制作例
#EasyStandard.bw

# ~~ここより上に使用する関数定義をコピペしておきます~~

0: default
	#文字の厚み=Duration
	changeDuration: 0.05
	#scaleは必ず4つとも同じ数字にする
	scaleStartRow: 0.5
	scaleStartHeight: 0.5
	scaleWidth: 0.5
	scaleHeight: 0.5

#あいうえお
20.0: hira_A #「あ」の壁を作る関数呼び出し
	addStartRow: -3
	addStartHeight: 1
21.5: hira_I #「い」の壁を作る関数呼び出し
	addStartRow: -1.5
	addStartHeight: 2.5
22.0: hira_U #「う」の壁を作る関数呼び出し
	addStartRow: 0
	addStartHeight: 4
22.5: hira_E #「え」の壁を作る関数呼び出し
	addStartRow: 1.5
	addStartHeight: 2.5
23.0: hira_O #「お」の壁を作る関数呼び出し
	addStartRow: 3
	addStartHeight: 1

# ~~以下略~~

各文字とも基準位置からのオフセットを反映させているので、関数上で等間隔で並ぶよう位置を設定してあげれば、文字間・行間にバラつきがでることはなく綺麗に文字が整列します。単語や文章も簡単に位置調整ができると思います。

画像10


今回自動生成した壁文字のBeatwalls関数は以下で公開しています。


これで誰でもいつでも好きな場所に簡単に文字を流せますね!

それではみなさま良きビーセイ壁文字ライフを~!!

この記事が気に入ったらサポートをしてみませんか?