ヘキサ日記 Blog

 

2017年9月22日

大きい画面は正義

        

普段は2~3ヶ月に1度くらいのペースで連絡が来る母親から”ドラクエXIをクリアした”報告と、

半年に1度くらいのペースで連絡が来る父親から”Switchの10月出荷分を予約できた”報告が、

最近同じくらいのタイミングで別々にきて、今日も世界は平和だなと確信したおのってぃです。

 

さくっと今日の本題です。

ついに発売しますね。待ちに待ったGvido。

昨年末からずっと発売を待っていたGvido。

知らない人はいないと思いますが、念のためにどういったものかざっくり復習しておきます。

・電子楽譜専用端末

・見開き2ページで、それぞれE-Ink電子ペーパー13.3インチパネル ・スタイラスで楽譜への書き込み可能

・画面タッチでページめくり、別売りのフットスイッチでもページめくり可能

・microSDで容量アップ可能

 

 

今は自炊した楽譜たちをiPadに入れて利用しているのですが、

・1ページ分の表示だとページめくりの頻度が多すぎて嫌になる

・iPadを横置きにして見開き表示にすると小さすぎて嫌になる

・ずっと画面をつけていると電池の消費量が異常で嫌になる(利用しているiPadは6年ほど前のものです)

(個人の感想です)

という状態で非常にストレスフルです。

重い楽譜を複数持ち運びしなくても良いという点で非常にメリットも大きいんですけどね。

 

それをすべて解消してくれる予定の素敵なGvido。

 

ところがです。 かなり値が張るので、すぐに入手するわけにはいかないです。

どこかの大富豪達が買い支えた結果、僕が買いやすい値段で販売されるようになることを願っています。心から。

 

それではもっと13インチパネルが流行ればいいのにと思いつつ今日はこの辺で終わりにしたいと思います。


        

こんにちはヨセミテです。

スープカレーは最近食べれていません。食べたい。

 

先日行われたCEDEC2017ではラウンドテーブルの司会という非常に貴重な経験をさせていただきました。改めてこの場で、来て下さった方々にお礼を言いたいと思います。ありがとうございました。


 

今回はMaya Python API2.0 でコマンドプラグインを作成していきたいと思います。

コマンドプラグインを作成すると新しいMELコマンド(Pythonだとcmdsライブラリ)の新しい命令を定義できます。

 

 ■ [ctrl + z]が効く高速なスクリプトが組めること

 ■ MELで自分からスクリプトを組むようなデザイナーさんが居る環境で使えるコマンドを増やせること

 ■ skinPercentの様な、そのまま使うと少々重過ぎるコマンドを場合に合わせたカスタマイズして高速なものを用意できること

 

などが主な利点だと思います。

 

「元に戻す機能」、「引数」を利用できるものを作ってみたかったので以下の二つのサンプルをベースに作成しました。

 ■ 公式ドキュメントの元に戻せるサンプル

 ■ 公式ドキュメントの引数を使うときのサンプル

 

作成してみたものが以下になります。頂点、もしくはメッシュ全体を選択して法線方向に引数の分だけ移動する、という処理です。


 

コメントが探り探り感あふれるのはご容赦下さい。探り探りです。 

# -*- coding: utf-8 -*-
import sys

import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui

kPluginCmdName = "blogCommand" # MELコマンド名

kShortFlagName = "-mv"         # 引数のショートネーム
kLongFlagName = "-moveValue"   # 引数のロングネーム

def maya_useNewAPI():
    # この関数が記述されているスクリプトはプラグインが生成されます、とMayaに伝えるための関数らしい
    pass
    
##########################################################
# Plug-in :メインの記述部分
##########################################################
class BlogCommandClass( om.MPxCommand ):
    
    def __init__(self):
        ''' Constructor. '''
        om.MPxCommand.__init__(self)
    
    def doIt(self, args):
        # doIt内部は一度だけ呼ばれるコマンドらしい
        # 公式のサンプルでredoItに実行処理書いてこっちでもredoitよびだしてたので習う

        # 引数を解析しているところ
        moveValue = self.parseArguments( args )
        
        # 対象のメッシュの諸々を取得
        moveTargetMesh_selList         = om.MGlobal.getActiveSelectionList()
        moveTargetMesh_dag             = moveTargetMesh_selList.getDagPath(0)
        self.moveTargetMesh_MFnMeshIns = om.MFnMesh(moveTargetMesh_dag)            # 選択対象のMFnMeshインスタンス

        moveTargetMesh_componentObject = moveTargetMesh_selList.getComponent(0)[1] # 選択頂点のコンポーネント
        if moveTargetMesh_componentObject.apiType() == 548:
            # 選択対象が頂点なら頂点のindexList取得
            moveTargetMesh_ComponentIns     = om.MFnSingleIndexedComponent(moveTargetMesh_componentObject)
            moveTargetMesh_selectedIndecies = moveTargetMesh_ComponentIns.getElements()                    # 選択頂点のindex
        else:
            # 選択対象が頂点なら頂点じゃないならindexListは頂点数分
            moveTargetMesh_selectedIndecies = xrange(self.moveTargetMesh_MFnMeshIns.numVertices)
            
        # undo処理のために元々の頂点座標を保存
        moveTargetPointArray_Origin     = self.moveTargetMesh_MFnMeshIns.getPoints(space = om.MSpace.kWorld)
        self.movePointArray_OriginClone = moveTargetPointArray_Origin[:]

        # 実行処理を繰り返し処理部分に書いているため、実行処理の呼びだし
        self.redoIt(moveTargetMesh_selList, moveTargetMesh_dag, moveValue,moveTargetMesh_selectedIndecies)
        
    def parseArguments(self, args):
        # 引数を読み込むための部分
        # 必須ではないが、このように分けると判り易いですよ、との事
        
        # 定義した引数が用いられているかどうかの確認
        # 定義した引数が異なるとココでエラーが発生する模様
        argData = om.MArgParser(self.syntax(), args)
        
        if argData.isFlagSet( kShortFlagName ):
            # flagValueにintにして受け渡ししてるそうで
            flagValue = argData.flagArgumentFloat( kShortFlagName, 0)
        return flagValue
            
    def redoIt(self, moveTargetMesh_selList, moveTargetMesh_dag, moveValue, moveTargetMesh_selectedIndecies):
        # 繰り返しをした際に呼び出される処理?
        # 公式のサンプルではココに実効的な処理を記述していたのでそれに習い、
        # ココに実効的な処理を書き、doit内部からredoitを呼び出す形式にする
        vertxNum = self.moveTargetMesh_MFnMeshIns.numVertices
        points   = self.moveTargetMesh_MFnMeshIns.getPoints(space = om.MSpace.kWorld)
        normals  = self.moveTargetMesh_MFnMeshIns.getVertexNormals(False, om.MSpace.kWorld)
        
        movedPosArray = om.MPointArray()    # 移動後の座標を格納する配列
        movedPosArray.setLength(vertxNum)   # 初期化

        for currentIndex in range(vertxNum):
            if currentIndex in moveTargetMesh_selectedIndecies:
                currentPos    = points[currentIndex]                   # 対象頂点の位置
                currentNormal = om.MVector(normals[currentIndex])      # 対象頂点の法線方向
                movedPos      = currentPos + currentNormal * moveValue # 移動後の座標
                movedPosArray[currentIndex] = movedPos
            else:
                movedPosArray[currentIndex] = points[currentIndex]     # 選択対象以外のindexはそのままの座標を格納

        self.moveTargetMesh_MFnMeshIns.setPoints(movedPosArray)        # 移動後座標を適用
    
    def undoIt(self):
        # undo時に呼び出すための逆処理を書く
        self.moveTargetMesh_MFnMeshIns.setPoints(self.movePointArray_OriginClone, space = om.MSpace.kWorld) # 確保しておいた元々の座標を適用

    def isUndoable(self):
        # 命令が実行可能かどうかを判定する部分
        # これが無いとundoできない模様
        return True

##########################################################
# Plug-in initialization.
##########################################################
def cmdCreator():
    return BlogCommandClass() 

def syntaxCreator():
    # syntaxCreaterは作成するコマンドに引数を持たせる場合は記述する必要がある。
    syntax = om.MSyntax()
    # ここで引数として持たせたいデータ型を指定する必要がある。
    syntax.addFlag( kShortFlagName, kLongFlagName, om.MSyntax.kDouble )
    
    return syntax
    
def initializePlugin( mobject ):
    mplugin = om.MFnPlugin( mobject )
    try:
        # 引数を持たせる場合と持たせない場合でココもちょっと変わる
        mplugin.registerCommand( kPluginCmdName, cmdCreator, syntaxCreator )
    except:
        sys.stderr.write( 'Failed to register command: ' + kPluginCmdName )

def uninitializePlugin( mobject ):
    mplugin = om.MFnPlugin( mobject )
    try:
        mplugin.deregisterCommand( kPluginCmdName )
    except:
        sys.stderr.write( 'Failed to unregister command: ' + kPluginCmdName ) 

 これをプラグインパスが通っている場所に置くと保存したファイル名でプラグインの読み込みに追加されています。

 

プラグインを読み込んだら、スクリプト内部で定義したMELコマンド、もしくはpythonのcmdsライブラリのコマンドで実行できます。

cmds.blogCommand(moveValue = 1) # Python 

で実行できます。

試してみたところ速度がちょっと微妙…。10万頂点くらいだと2~3秒かかってしまいました。選択頂点で判定する処理をいれた辺りでとても遅くなったのでその辺にボトルネックがありそう…。入れる前だと100万頂点でも1秒掛からなかったのでプラグイン自体はとても早いです。

 

実行すると以下のように動きました。[ctrl + z]も効いて引数で移動量もちゃんと変わっているようです。

  img4

コマンドプラグインはcmdsでスクリプトを組むことに比べればやはり時間的なコストはかかってしまいます。が、頂点数が増えてくるとその強みが実感できます。Maya Python API2.0 ならC++で作成するよりはるかにテストやイテレーションをまわすのが楽なので是非試してみてください。

 

では。


2017年9月20日

読書の秋

        

すっかり涼しくなりましたね。皆様いかがお過ごしでしょうか。
東京デザイナーのとりっぴーです

 

さて、季節は読書の秋ということで、

本日はUI/UXに関する書籍のご紹介をいたします。

 

UIのデザインは一見目立ちにくいかもしれませんが、

ゲームとプレイヤーを繋ぐ大切な要素を担っており、プレイフィールにも影響します。

UIが分かりにくいと、そもそもゲームをどう遊べばいいのか伝わらないことになってしまいます

そして悩ましいことに、そのゲームや媒体によって「何が最適なUIなのか」はそれぞれ違い、
一概に正解があるというものでもありません。
実装したものをテストプレイして「触った感覚はどうか」など
都度チェックしながら開発していくのですが、
本当に奥が深い分野で、私も勉強しながら試行錯誤の日々でございます

 

また普段、色々なゲームを触ってゲームのUIを勉強させていただくことがありますが、
基本的にリリースされている完成した製品を触るということが多いです。
どのような経緯でこのようなデザインに至ったのか、

ある程度は想像することができるかもしれません。

しかし、開発中の試行錯誤はCEDECや勉強会など以外では、
なかなか知る機会が少ないのではと思います。

 

そこで、「ゲームUIの書籍って無いのかな?」と探していて

こちらの一冊にたどり着きましたのでご紹介します

 

20170920_book.jpg

「売れるゲームのUI/UX 制作現場の舞台裏」

 

この本では、UI/UXとは何か?といった基本的な知識が簡潔にまとめられており、
誰にとっても読みやすい内容だと感じました
また、スマートフォン、コンソール、アーケード、VRといった様々なプラットフォームで

それぞれのゲームタイトルの開発事例が載っていることが特徴的です。
実際の開発中のゲーム画面のラフデザインから完成するまでの画面改修の変移、

そのゲームやプラットフォームならではの工夫も解説・収録されています

 

UIを作られている方も、そうでない方も、
宜しければチェックしてみてください

 

ちなみに、ヘキサドライブでは書籍を申請をすると
会社費用で購入してもらえる制度があります
本棚には勉強のために色々な分野の参考書が並んでいて
借りることができ、とても重宝しています
(こちらの書籍も会社で購入したものです。)

 

それでは、本日はこちらで失礼します


2017年9月19日

背景を見よう

        

こんにちは。

3連休いかがお過ごしでしたでしょうか、ブリテンです。

 

私はと言いますと、いかんせん台風でしたので、

『ゲーム・オブ・スローンズ』をシーズン7まで見て、

またシーズン1から走りなおす作業をしていました。

20170919_got

 

人気作なのでご存知の方もいらっしゃると思いますが、

『ゲーム・オブ・スローンズ』(原題:Game of Thrones)は

アメリカのケーブルテレビ局HBOが制作する、

イングランド・薔薇戦争をモデルとした、ファンタジードラマです。

 

このドラマ、ウェスタロスと言う大地で

個性豊かな登場人物同士が織り成す群像劇が

とても面白い! 先が気になる!

 

のですが、

それと同じくらい、背景が素晴らしいです。

 

ウェスタロス北部の「ウィンターフェル」は北アイルランド

ウェスタロス中南部の王都「キングスランディング」はクロアチア

ウェスタロス最南端「ドーン」はスペイン

で撮られており、それぞれ街の特色を出しています。

特にドーンは中世ヨーロッパ的なことを言えば、レコンキスタ完了前なので、

スペイン+イスラムの雰囲気を出しています。

 

これらはすべて架空の場所です。

しかし、実際にその場所が世界に存在したら・・・

いやむしろウェスタロスとその周辺諸国が実際に存在したら・・・

という想定で作られているため、リアリティがあります。

 

 

その分制作費がハンパないことになっているそうですが、

登場人物やドラマを立たせるためにも、

背景のリアリティも追求しているのですね。

 

みなさんも、もしご覧になられる際は

「背景」にも注目してみてくださいね。

 

 

 

 

 


        

ラーメン屋でスタンプカード出すのを忘れがちな某(なにがし)です

 

過去にシュンスケさんが32bitカラー画像を16bitカラーに減色する方法を紹介してくれました。

 

32とか16とかその5
↑今回関係するのはこのへん

 

「組織的ディザ法」「誤差分散法」を試して、誤差分散法が結構イイカンジという話でした。

 

私のチームで、シュンスケさんの実装を借用してみたところ、
それなりにイイカンジではありますが、元画像からの劣化がやっぱり気にはなりました。

 

元画像
20170915_original

 

減色後
20170915_rgba4444

 

※画像は「魔法パスワード1111」で使われている背景画像です。
 減色による画質劣化がわかりやすいものを選びました。

 

画像全体にノイズのようなドットが目立ちます

 

そもそもこの元画像、見ての通り不透明な背景画像です。

 

つまり、アルファチャンネルを使っていないので、
RGBA各チャンネルを4bitずつ表現しているRGBA4444形式では、
アルファチャンネルの4bitがすべて無駄になっているということになります。

 

この無駄な4bitをうまく利用してやれば、さらにイイカンジにできそうです

 

16bitカラーと言っても、RGBA4444だけでなく、RGB565という形式もあります。
名前の通り、RGBがそれぞれ565bitで構成されて16bitとなります。

 

アルファチャンネルがないので、不透明画像に適しています。
緑(G)が1bitだけ他のチャンネルより大きいのが面白いですね。
1bit増えると表現可能な色数が倍になりますので、大きな差になります。

 

それでは、シュンスケさんの実装をRGBA4444からRGB565に対応させてみます

 

特定の桁数から、任意の桁数に収まるよう端数を切る処理が必要なので、
例えば、10桁から6桁に落とすなら、上位6桁を持ってきて、下位4桁を捨てる考えでいいでしょう。

 

32bitフルカラーからの減色の場合、1チャンネルあたり8bit(1byte)なので、
Nビットに落とすなら、上位Nビットを取ってくればOKです。

 

// 1ピクセルあたりのバイト数
private const int PIXEL_BYTES = 4;

// 1バイトあたりのビット数
private const int BYTE_BITS = 8;

// 減色モード
private enum Mode
{
	RGBA4444,
	RGB565,
}

// チャネル
private enum Channel
{
	R,
	G,
	B,
	A,
}

private static uint quantizeChannel(uint value, Mode mode, Channel channel)
{
	var bitCount = BYTE_BITS;

	switch(mode) {
	case Mode.RGBA4444:
		bitCount = 4;
		break;
	case Mode.RGB565:
		if(channel == Channel.A) {
			bitCount = 0;
		}
		else if(channel == Channel.G) {
			bitCount = 6;
		}
		else {
			bitCount = 5;
		}
		break;
	default:
		break;
	}

	if(bitCount >= BYTE_BITS) return value;
	if(bitCount <= 0) return 0xFF;

	// 上位bitCountビット分を返す
	var shiftBits = BYTE_BITS - bitCount;
	return value & (~0u << shiftBits);
}

private static uint channelFilter(uint org, int x, int y, ref double[,] err, Mode mode, Channel channel)
{
	// 蓄積された誤差を加えて近似
	var tmp = (err[x, y] + org);
	if(tmp < 0) {
		tmp = 0;
	}
	if(tmp > 255) {
		tmp = 255;
	}
	var c = quantizeChannel((uint)Math.Round(tmp), mode, channel);

	// 誤差を分散して蓄積
	double e = tmp - c;
	for(int i = 0; i < DISPERSION_TBL.Length; ++i) {
		var p = DISPERSION_TBL[i];
		var xIdx = x + p.offsetX;
		var yIdx = y + p.offsetY;
		var xLen = err.GetLength(0);
		var yLen = err.GetLength(1);
		if(xIdx < 0 || xIdx >= xLen ||
		yIdx < 0 || yIdx >= yLen) continue;

		err[xIdx, yIdx] += (p.rate * e);
	}

	return c;
}

private static void filter(ref byte[] bmp, int w, int h, Mode mode)
{
	// 誤差格納用の領域を確保
	var errR = new double[w, h];
	var errG = new double[w, h];
	var errB = new double[w, h];
	var errA = new double[w, h];

	for(int i = 0; i < w; ++i) {
		for(int j = 0; j < h; ++j) {
			errR[i, j] = 0;
			errG[i, j] = 0;
			errB[i, j] = 0;
			errA[i, j] = 0;
		}
	}

	for(int y = 0; y < h; ++y) {
		for(int x = 0; x < w; ++x) {
			var index = (y * w + x) * PIXEL_BYTES;
			uint r = bmp[index + 0];
			uint g = bmp[index + 1];
			uint b = bmp[index + 2];
			uint a = bmp[index + 3];

			r = channelFilter(r, x, y, ref errR, mode, Channel.R);
			g = channelFilter(g, x, y, ref errG, mode, Channel.G);
			b = channelFilter(b, x, y, ref errB, mode, Channel.B);
			a = channelFilter(a, x, y, ref errA, mode, Channel.A);

			var color = Color.FromArgb((int)a, (int)r, (int)g, (int)b);
			bmp[index + 0] = color.R;
			bmp[index + 1] = color.G;
			bmp[index + 2] = color.B;
			bmp[index + 3] = color.A;
		}
	}
}

 

では、出力を比較してみましょう。

 

元画像
20170915_original

 

RGB565
20170915_rgb565

 

RGBA4444
20170915_rgba4444

 

全然違いますね。RGB565なら、ぱっと見では元画像と同じに見えます

 

ところで、Unity(5.4以前)でテクスチャフォーマットを16bit Colorで指定したとき、
元画像が不透明だと自動でRGB565に変換されます(※)が精度は良くないです・・・

 

ここで今回の手法を適用した画像を使うと、Unityの自動変換による劣化をかなり防げます

 

※Unity 5.5からテクスチャフォーマットの直接指定はプラットフォーム別にしかできなくなりました。
 旧バージョンからアップグレードした時には、metaに中途半端な設定が残ることもあるので注意しましょう。
 エディタ上で「RGB 16 bit」と表示されるものが「RGB565」に相当します。
(参考:Unity – Manual: Texture compression formats for platform-specific overrides

 

こういう減色ツールが手軽に使えるものとしてなかったりしますが、
原理は簡単なので作ってしまえば何かと融通がききそうです


2017年9月14日

ちょきんロボ

        

最近は涼しくなって過ごしやすくなってきましたねえ。モリタです。
でもこういった季節の変わり目は体調を崩しやすいのでご注意を・・!

 

・・と言いながら、少し遡って子供たちが夏休みの時。

二男から「小学校の課題で↓のようなものを工作したい。手伝って」と来ました。

20170914_01貯金箱設計図

おーなんだこんな貯金箱考えたのか!なかなかいいじゃないヨシヨシ

 

でもどうすれば実現するんだこれ・・と思ってちょっと悩みましたが、

ネットで調べると(→【貯金箱 お金の分け方】で検索。夏休みの工作系としては思いのほか良くあるものなんですね)

 

基本は「サイズが小さい順にお金が通る穴に落として選別していくようにする」だったので

脳内シミュレーションしてから、作ってみました!

この時点でいくつかの機能(ボタンでお金が出てくるとか)は省略する方向で・・。

 

————————————————————–

 

材料を100円ショップで購入

 20170914_02貯金箱_材料

・プラ板     x1

・スポンジシート x1

・厚紙      x1
・接着剤     x1
・お金を入れる箱 x3
定規とカッターとはさみとボールペンと下敷きとテープはあったのでプライスレス。

ごちゃごちゃしていますが、途中まで作ってから「ネタになるかも」と撮った写真じゃないです。

 

————————————————————–

 

「プラ板」に、お金のサイズが小さい順に落とす穴を開けて(小さめに開けて徐々に削るように大きくしていきました)

       

       「スポンジシート」でお金を転がす床?を付けて

               

               「厚紙」で中の仕切りや外部を作りましたー。
20170914_03貯金箱作成中
(後でメンテナンスしやすいように「接着剤」「テープ」を使い分けて組み立て)

 

————————————————————–

 

(機能としての)完成が以下。

20170914_04貯金箱完成

動画がこちら

 

————————————————————–

 

実際のところ、そこまで精度は高くないので意図通り落ちないこともあるのですが・・まあ良しとします!

二男に見せたら満足していたし! さあ後は色でも塗って持っていきなさい!

 

と、伝えたらここで奥さんから

「自分一人で全部作って二男手伝ってないじゃない」と、おっしゃるとおりのご指摘・・。

色塗ればいいかなと思って・・と説明すると

「それじゃ二男が作ったと言わない」と、おっしゃるとおりのご指摘・・。

 

・・時間の都合上、工作か作文のどちらかを提出する課題だった。とのことで作文になりました

 

・・あーなんていうかこれ、仕事でもあることですね!(え?)だから気にしない!(え?)

 

現実世界のアナログ作業はアンドゥも効かないしバックアップも取れないし、

とても面倒ですが、たまにやると楽しいですね!

 

それではまたですー


        

みなさんこんにちは。グリフォンです

 

さて今回はシリーズでお届けしてきた「フォントのDistanceFieldTexture生成ツールをつくる」のその5。これが最後です。

 

大まかな処理の流れは以下

フォントを1文字ずつ読み出してピクセル情報を取得

ピクセル情報からDistance Field情報を生成

CharacterInfoを生成

アトラステクスチャに書き込む

アトラステクスチャとFontSettingsを出力保存

 

 

今回はを説明していきます。(ラスト

// アトラステクスチャの書き出し
File.WriteAllBytes("D:/FontDistanceFieldGenerator/Project/Assets/" + _OutputFontTextureName, _OutputFontTexture.EncodeToPNG());

// フォント設定ファイルの書き出し
{
	// 出力用にフォントクラスを生成
	var outputFont = new Font(_TextArea.font.name);
	outputFont.characterInfo	= _OutputFontModel._chars._charInfos.ToArray();

	// 既存フォント情報のシリアライズオブジェクトを生成
	var srcSerializedFont		= new SerializedObject(_TextArea.font);
	
	// 出力フォルト情報のシリアライズオブジェクトを生成
	var dstSerializedFont		= new SerializedObject(outputFont);
	
	// 必要情報をコピー
	dstSerializedFont.FindProperty("m_Tracking").floatValue		= srcSerializedFont.FindProperty("m_Tracking").floatValue;
	dstSerializedFont.FindProperty("m_LineSpacing").floatValue	= srcSerializedFont.FindProperty("m_LineSpacing").floatValue;
	dstSerializedFont.FindProperty("m_CharacterSpacing").intValue	= srcSerializedFont.FindProperty("m_CharacterSpacing").intValue;
	dstSerializedFont.FindProperty("m_CharacterPadding").intValue	= srcSerializedFont.FindProperty("m_CharacterPadding").intValue;
	dstSerializedFont.FindProperty("m_FontSize").floatValue		= srcSerializedFont.FindProperty("m_FontSize").floatValue;
	dstSerializedFont.FindProperty("m_Ascent").floatValue		= srcSerializedFont.FindProperty("m_Ascent").floatValue;
	dstSerializedFont.ApplyModifiedProperties();
	
	// 書き出し
	saveAsset(outputFont, "Assets/" + _TextArea.font.name + ".fontsettings");
}

 

今回は2つのファイルの書き出しを行います。

 

1つ目は、前回まで生成したアトラステクスチャを File.WriteAllBytes() で書き出します。(保存パスはご自由に)

 

2つ目は、 fontsettings を書き出します。これはで生成した文字情報のファイルですが、で生成した情報だけでは足りないので、元にしたフォントからコピーします。

 

fontsettings の書き出しは Unity のアセットとして保存する必要が有るので saveAsset() という関数で行っています。

private Object saveAsset(Object obj, string filePath)
{
	// アセットデータベースを検索して既存のファイルがあれば取得する
	var existAsset = AssetDatabase.LoadMainAssetAtPath(filePath);
	if( existAsset != null ) {
		// 新しくアセット生成
		EditorUtility.CopySerialized(obj, existAsset);
		AssetDatabase.SaveAssets();
		return existAsset;
	}
	else {
		// 既存のアセットに上書き
		AssetDatabase.CreateAsset(obj, filePath);
		AssetDatabase.Refresh();
		return obj;
	}
}

 

以上で、Distance Field Textureを使用したフォントを生成するツールの完成です

 

 実は、このツールは既にリリースされている以下のタイトルで使用しています。

どんな感じの見た目か気になる方は是非アプリをプレイして見てください。

 

20170913_Pouch

『 アイテム代は経費で落ちない~no item, no quest~』

 

20170913_Wand

『魔法パスワード1111』

 

長々とお付き合いいただき、ありがとうございます。

この内容が、みなさんのゲーム制作のお役に立てば幸いです


2017年9月12日

美しき水景を求めて

        

こんにちは。東京デザイナーの足立です。

 

今回はエフェクト関係の書籍「イラストでわかる物理現象 CGエフェクトLab.」の紹介をと思っていたら、
発売日前にブログ順が?!
見てないものを紹介するわけにもいかないので・・・

 

 

アクアなお話を。

 

ネットでアクアリウムを検索すると幻想的で美しい水景がたくさん出てきますね。
そんな水景に魅せられて&アクア部部長に勧誘されてアクアリウムを始めてから約7ヶ月。

 

IMG_0987

 

 

ただ水草が多いだけの水槽・・・

最初は背景デザイナーの腕の見せ所だぜ!と意気込んでいたのですが、
苔との戦いやらで、結局のところ維持するのが精一杯。

 

小さな水槽の中で奥行き感を出すために
遠近法を用いて手前には大きな岩や水草、奥には葉の小さな水草を配置、
さらには空気遠近法も利用して、手前には濃い色、奥には淡い色の水草を配置しつつ
黄金比を意識した構図に三尊石組を取り入れて~

ってやるつもりだったんですけどねぇ。

 

まぁ初めてだしここまで育っただけでも良しとして、
次はどんなレイアウトにしようかと日々妄想してます。

 

あ、ほら、常にレイアウトのことを考えてるなんてデザイナーっぽいでしょ。

 

 


        

ちょっと前ですが、ダヴィンチのパラドックスが解明されるかもしれない

という記事をヤフーニュースで読んで、

 

レオナルド・ダ・ビンチ以来の「泡の謎」に迫る

https://news.yahoo.co.jp/byline/ishidamasahiko/20170818-00074648/

 


なになに?泡の動きは「慣性モーメント」が関係している?

「慣性の法則」?「トルク、モーメント」?

なにやらフルイド(流体力学)とか生体工学アニメーションに関係ありそうだぞ?

そういや、青木慎也が腕ひしぎ十字固めをはずすの解説の時に

トルクとモーメントを使ってアカデミックな解説をしてたかも?

ぶっちゃけこの記事よく分からん。とりあえず模写してみるか……


 

と頭のアンテナが反応しワクワクした東京開発モーションデザイナーのおおみや(む)です。

 

 

本題に入って、これまでのブログはアニメーションについて実践的な内容でしたが、

今日は座学。本のご紹介をしたいと思います。

 


511QtSTsDGL._SX339_BO1,204,203,200_

アフォーダンス入門

著:佐々木正人

https://www.amazon.co.jp//dp/4061598635/


 

アフォーダンスと聞いて、ちょっと小難しいな?中二っぽいような、

近寄りがたい感じの、よく分からない専門用語だなと思う人は多いかもしれません。

 

アフォーダンスとは引用すると、生態心理学者ジェームズギブソンの造語で

 

英語のアフォード(afford)「与える、提供する」などを意味し、

 

アフォーダンス(affordance)は「環境が動物に提供するもの、

用意したり備えたりするもの」<~中略~>ぼくら動物の行為の

「リソース(資源)」になることである。

動物の行為はアフォーダンスを利用することで可能になり、

アフォーダンスを利用することで進化してきた。

 

そして、

 

アフォーダンスはフィジカルであり、バイオロジカルでもあり、

サイコロジカルなことである。物であり、生きものに関係しており、

そしてぼくらが「こころ」とよんでいる環境と行為との

かかわりプロセスの中心にあることである。

生態心理学をはじめることは、だから、物理学と生物学と心理学との

間に今ある高い垣根を超えようとすることでもある。

 

ということらしいです(汗)

 

いっそう分からなくなりましたが、解剖学を引き合いに出して少々強引に理解すると

どちらも科学的でありながら、解剖学がいきものの動く仕組みを解剖して

内へ内へ探求するのに対し、生態心理学外側(環境)から徹底的に観察

し、いきものが動く仕組みを探求していく学問で、その発想の大元にあるのが

アフォーダンスということになります。

 

そして「こころ」を魂(アニマ)とか観念的でなく環境と行為の観察

分析し、科学的に解明していこうというわけです。

 

なにやら「クランカー」「ダーウィニスト」、

もとい「機械主義者」「生体工作者」の対立の様で勝手に盛り上がってきましたよ!

 


気になった方はこちら↓

51ofkkEbkwL._SX333_BO1,204,203,200_

リヴァイアサン: クジラと蒸気機関

著:スコット ウェスターフェルド

https://www.amazon.co.jp//dp/4150119333/

 51t1pHNaltL._SX343_BO1,204,203,200_

スキズマトリックス

著:ブルース・スターリング

https://www.amazon.co.jp//dp/4150107513/


 

少々脱線しましたが、その入門ということで登場するのがダーウィンです。

ダーウィンは進化論で有名ですが、動植物の生態の研究でも有名で、その観察、分析などにより、

動物行動学の基礎を築いた人でもあります。

 

サンゴからはじまり、ミミズ、カエルなどの環境に基づく生き物の研究の解説などを例に

環境といきものの生態を解説しつつ、その周りの環境、地面や水や大気、波や重力や光について

突っ込んで解説していきます。そして、ここで重要なのは環境からの刺激に対する反射ではない

ということですね。

環境が生態に影響し、知覚するというのは外界からの刺激に対する

単なる反射ではないということを説明しています。

 

ピンと来る人は、これは

プロシージャルなデザインやサンドボックス型のゲーム開発に活かせる

のではないかな?と思いませんか。

 

もう一つ「ボーンスペース」と「協調」について、

 

ボーンスペースとは、人間にはほぼ100(細かく数えれば200)の骨があり、

すべての骨が筋で一つにリンクしている。したがって身体がするあらゆる接触では、

すべての骨と筋は一つのシステムとして動く。ギブソンは骨を中心として

一つのシステムとして挙動する大きな器官、「ボーンスペース(骨格空間)」

とよんだ。

 

またここで出てくるのが、ロシアの生理学者、ニコラス・ベルンシュタインです。

 

memo

※ベルンシュタインの「協調の原理」の図

 

余談ですが、ロシアの近代格闘術、システマもニコラス・ベルンシュタインの考えを

元に生み出されたと言われています。

 


気になった方はこちら↓

ダウンロード

デクステリティ 巧みさとその発達

著:ニコライ・アレクサンドロヴィッチ ベルンシュタイン

https://www.amazon.co.jp//dp/4760828214


 

ここで言われているのは、静止した姿勢を人間がとる時、環境に対して

常に定位を調整し続けている。静止した姿勢の「動き」を取り続けている

ということに気付きます。

 

引用すると、

機械の制御に使われる動きはエンジンの回転を車輪に伝えるシリンダー

の動きのように「押す力」を利用できる。機械の制御のための力は

固いもので押す力なので前もってきちんと値を決めておくことが出来る。

 

しかし、動物のあらゆる筋は柔軟であり「引く力」だけで動いている。

筋は関節を押せない。したがって、機械のように力の値を前もって

決めておいてそれで制御するということはできない。

身体の制御の原型がこのようなものであると考えると、一つの事実が

あきらかになる。

 

それは身体を制御するためには、筋も骨もいつも休み無く動き続けて

いなければならない、ということである。

 

ここでピコーンッと来た人は、HumanIKやゲームのプレイヤーの

ビヘイビア、AI制御、はたまた「不気味の谷」問題に活かせる

のではないかな?と思ったのではないでしょうか。

 

この後、運動生理学とか認知科学に関係するさらに面白い内容が書いてある

のですが、ご紹介はこの辺で。

ゲームの常識を変えたい!と思っている方は、ひょっとしたら新しい世界が拓けますよ!

 

次回はまた実践に戻れればなぁと思います。

それではまた。

 

 

 

 


        

黒くてしゅわしゅわしてて甘くて美味しいあの飲み物

そう、コーラ

幸せを分け合うときには欠かせないですよね

そんなコーラを毎日2㍑飲む生活をはじめました

こんにちは、だっちです

 

先日プレゼントに貰ったコーラ24㍑があるので、

仕事をしながら飲んでいるといつの間にかペットボトルが空に…なんて日々を送っています。

健康のためには少し自制しないといけませんね

 

 

さて、前回に引き続きExcelで作成したデータから、

ゲーム内で扱いやすい形に変換するためのプログラムを実装していこうと思います。

 

前回は「型を生成するために必要な情報」を読み出すところまで出来たので、

今回は「クラスファイルの生成」をしていきたいと思います。

 

前回作成したプログラムを改良して、クラスファイルを生成していきます。

コードはこんな感じです。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NPOI.SS.UserModel;

namespace NPOITest
{
	class Program
	{
		// クラス情報
		private class ClassInfo
		{
			public string						name					{ get; }		//!< クラス名
			public string						desc					{ get; }		//!< クラス説明
			public List		memberVariableInfos		{ get; }		//!< メンバ変数情報

			// @brief	コンストラクタ
			// @param	[in]	name					クラス名
			// @param	[in]	desc					クラス説明
			// @param	[in]	memberVariableInfos		メンバ変数情報
			public ClassInfo(string name, string desc, List memberVariableInfos)
			{
				this.name					= name;
				this.desc					= desc;
				this.memberVariableInfos	= memberVariableInfos;
			}
		}

		// メンバ変数情報
		private class MemberVariableInfo
		{
			public string		name		{ get; }		//!< 変数名
			public string		type		{ get; }		//!< 型
			public string		desc		{ get; }		//!< 変数説明

			// @brief	コンストラクタ
			// @param	[in]	name	変数名
			// @param	[in]	type	型
			// @param	[in]	desc	変数説明
			public MemberVariableInfo(string name, string type, string desc)
			{
				this.name	= name;
				this.type	= type;
				this.desc	= desc;
			}
		}

		private const string	TYPEDEF_SHEET_NAME			= "typedef";		//!< 「型情報定義」シート名

		private const int		DATA_NAME_ROW				= 0;				//!< データ名行
		private const int		DATA_NAME_CELL				= 1;				//!< データ名列

		private const int		DATA_DESC_ROW				= 1;				//!< データ説明行
		private const int		DATA_DESC_CELL				= 1;				//!< データ説明列

		private const int		MEMBER_VARIABLE_START_ROW	= 10;				//!< メンバ変数開始行
		private const int		MEMBER_VARIABLE_NAME_CELL	= 1;				//!< メンバ変数名列
		private const int		MEMBER_VARIABLE_TYPE_CELL	= 2;				//!< メンバ変数型列
		private const int		MEMBER_VARIABLE_DESC_CELL	= 3;				//!< メンバ変数説明列
		private const int		MEMBER_VARIABLE_MAX			= 1024;				//!< メンバ変数最大許容数

		// @brief	エントリポイント
		public static void Main(string[] args)
		{
			// エクセルファイル入力受付
			Console.Write("Please input excel file path : ");
			var srcPath = Console.ReadLine();

			// 読み込み
			ClassInfo classInfo = null;
			readExcelFile(srcPath, out classInfo);

			// クラスファイル書き出し先入力受付
			Console.Write("Please input class file output directory : ");
			var outDirectory = Console.ReadLine();

			// 書き出し
			writeClassFile(outDirectory, classInfo);
		}

		// @brief	エクセルファイルの読み込み
		// @param	[in]	srcPath		読み込みファイル名
		// @param	[out]	classInfo	クラス情報
		// @return	なし
		private static void readExcelFile(string srcPath, out ClassInfo classInfo)
		{
			classInfo = null;

			// ファイル存在確認
			if( !File.Exists(srcPath) ) {
				Console.WriteLine("file not exists.");
				return;
			}

			var className			= string.Empty;
			var classDesc			= string.Empty;
			var memberVariableInfos	= new List();
			using( var fs = new FileStream(srcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) ) {
				var workbook = WorkbookFactory.Create(fs);

				// 中にあるシートを順番に取得
				foreach( ISheet sheet in workbook ) {
					if( sheet == null ) continue;

					// シート名表示
					Console.WriteLine("	Name : " + sheet.SheetName);

					// シートの名前が「型情報定義」シート名だったら、型情報を読み込み
					if( sheet.SheetName == TYPEDEF_SHEET_NAME ) {
						Console.WriteLine();

						Console.WriteLine("Data");
						// データ名を取得
						className = getCellValue(sheet, DATA_NAME_ROW, DATA_NAME_CELL);
						if( string.IsNullOrWhiteSpace(className) ) {
							Console.WriteLine("DataName acquisition failed...");
						}
						else {
							Console.WriteLine("	Name : " + className);
						}

						// データ説明を取得
						classDesc = getCellValue(sheet, DATA_DESC_ROW, DATA_DESC_CELL);
						if( string.IsNullOrWhiteSpace(classDesc) ) {
							Console.WriteLine("DataDesc acquisition failed...");
						}
						else {
							Console.WriteLine("	Desc : " + classDesc);
						}

						// メンバ変数を取得
						Console.WriteLine("MemberVariables");
						for( int i = 0, row = MEMBER_VARIABLE_START_ROW; i < MEMBER_VARIABLE_MAX; ++i, ++row ) {
							// メンバ変数名を取得
							var memberName = getCellValue(sheet, row, MEMBER_VARIABLE_NAME_CELL);
							if( string.IsNullOrWhiteSpace(memberName) ) {
								Console.WriteLine("MemberName acquisition failed...");
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("End Of Member Variables");
								break;
							}

							Console.WriteLine("	[" + i + "]");
							Console.WriteLine("		Name : " + memberName);

							// メンバ変数型を取得
							var memberType = getCellValue(sheet, row, MEMBER_VARIABLE_TYPE_CELL);
							if( string.IsNullOrWhiteSpace(memberType) ) {
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("MemberType acquisition failed...");
							}
							else {
								Console.WriteLine("		Type : " + memberType);
							}

							// メンバ変数説明を取得
							var memberDesc = getCellValue(sheet, row, MEMBER_VARIABLE_DESC_CELL);
							if( string.IsNullOrWhiteSpace(memberDesc) ) {
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("MemberDesc acquisition failed...");
							}
							else {
								Console.WriteLine("		Desc : " + memberDesc);
							}

							memberVariableInfos.Add(new MemberVariableInfo(memberName, memberType, memberDesc));
						}
					}
				}
			}

			classInfo = new ClassInfo(className, classDesc, memberVariableInfos);
		}

		// @brief	クラスファイルの書き出し
		// @param	[in]	outDirectory	書き出し先ディレクトリ
		// @param	[in]	classInfo		クラス情報
		// @return	なし
		private static void writeClassFile(string outDirectory, ClassInfo classInfo)
		{
			// 入力情報が不正なら終了
			if( string.IsNullOrWhiteSpace(outDirectory) ) {
				Console.WriteLine("outDirectory is invalid....");
				return;
			}
			if( classInfo == null ||
				string.IsNullOrWhiteSpace(classInfo.name) ||
				string.IsNullOrWhiteSpace(classInfo.desc) ||
				classInfo.memberVariableInfos == null ) {
				Console.WriteLine("classInfo is invalid...");
				return;
			}

			// 指定ディレクトリがなければ作成
			if( Directory.Exists(outDirectory) ) {
				Directory.CreateDirectory(outDirectory);
			}

			// ソースコード作成
			var sourceCode = new StringBuilder();

			// クラス開始
			sourceCode.AppendLine("// @class	" + classInfo.name);
			sourceCode.AppendLine("// @brief	" + classInfo.desc);
			sourceCode.AppendLine("public class " + classInfo.name);
			sourceCode.AppendLine("{");

			// プロパティ開始
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				sourceCode.AppendLine("	public " + memberVariableInfo.type + " " + memberVariableInfo.name + " { get; } //!< " + memberVariableInfo.desc);
			}
			sourceCode.AppendLine();
			// プロパティ終了

			// コンストラクタ開始
			sourceCode.AppendLine("	// @brief	コンストラクタ");
			// コンストラクタコメント
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				sourceCode.AppendLine("	// @param	[in]	" + memberVariableInfo.name + "	" + memberVariableInfo.desc);
			}
			// 引数
			var constructorArgs = new StringBuilder();
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				constructorArgs.Append(memberVariableInfo.type + " " + memberVariableInfo.name + ", ");
			}
			// 最後の不要な「, 」削除
			constructorArgs.Remove(constructorArgs.Length - 2, 2);
			sourceCode.AppendLine("	public " + classInfo.name + "(" + constructorArgs.ToString() + ")");
			sourceCode.AppendLine("	{");
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				sourceCode.AppendLine("		this." + memberVariableInfo.name + " = " + memberVariableInfo.name + ";");
			}
			sourceCode.AppendLine("	}");
			// コンストラクタ終了

			sourceCode.AppendLine("}");
			// クラス終了

			// 書き出し
			using( var sw = new StreamWriter(outDirectory + classInfo.name + ".cs", false, new UTF8Encoding(true)) ) {
				sw.Write(sourceCode.ToString());
			}
		}

		// @brief	セルの値を取得
		// @param	[in]	sheet		シート
		// @param	[in]	rowIndex	行インデックス
		// @param	[in]	cellIndex	列インデックス
		// @return	セルの値
		private static string getCellValue(ISheet sheet, int rowIndex, int cellIndex)
		{
			if( sheet == null ) return string.Empty;

			// 行を取得
			var row = sheet.GetRow(rowIndex);
			if( row == null ) return string.Empty;

			// 行からセルを取得
			var cell = row.GetCell(cellIndex);
			if( cell == null ) return string.Empty;

			// セルが取得できたら中身を文字列化
			return cell.ToString();
		}
	}
}

 

実際に生成されたコードはこんな感じです。

// @class	Weapon
// @brief	武器
public class Weapon
{
	public uint id { get; } //!< ID
	public string name { get; } //!< 名前
	public string desc { get; } //!< 説明
	public int attack { get; } //!< 攻撃力

	// @brief	コンストラクタ
	// @param	[in]	id	ID
	// @param	[in]	name	名前
	// @param	[in]	desc	説明
	// @param	[in]	attack	攻撃力
	public Weapon(uint id, string name, string desc, int attack)
	{
		this.id = id;
		this.name = name;
		this.desc = desc;
		this.attack = attack;
	}
}

 

これで、「クラスファイル」を生成するところまで完成しました

 

 

次回は、実際にデータファイルの書き出しをしていきたいと思います。

ではでは~


ヘキサブログ ピックアップ



過去の日記はこちら

2017年9月
« 8月    
 123
45678910
11121314151617
18192021222324
252627282930