Quantcast
Channel: HEXA BLOG –株式会社ヘキサドライブ | HEXADRIVE | ゲーム制作を中心としたコンテンツクリエイト会社
Viewing all 731 articles
Browse latest View live

遊び方のカテゴリ

$
0
0

疾きこと風の如く、徐かなることミッツです🍷

ある1つのゲーム作品について論じたり評価をおこなう時、
なぜだか、それがとても難しいものになる、ということがよくあります。
・伝えたいことがまとまらず茫洋としてしまう
・言ってることが共感や理解されない
・なぜか「的外れ」だと言われてしまう
・同じように面白いと言ってるのに結論が食い違う

おお、人類はなにゆえゲーム1つ語り合うこともままならないのか…。

なぜなら、ゲームのどの要素を求めてプレイを始めたか、
プレイヤーによって様々だからです。
例えば、MMORPGをプレイする時、物語を重んじている人もいれば、
キャラ育成を楽しんでいる人、戦闘ハンティング大好きな人、仲間との
交流こそが中心の人なんかもいます。
異なる部分に価値を見出している者同士が、お互いの価値観をよく
知らないままに、そのMMORPGについて議論を交わしたとしても、
両者間に横たわる溝の深さを知ることにしかならないでしょう。

つまるところ、ゲームについて語らう時、自分や相手がどういった
プレイヤーなのか、その性質を理解することが大切だったりします。
そこで今回のブログでは、トレーディングカードゲームの代表作
『マジック・ザ・ギャザリング』の開発元が定義している
有名な『ティミー』『ジョニー』『スパイク』の3つのプレイヤーの
カテゴリについて紹介してみたいと思います🔔

───────────────────────────────

🌟3つのカテゴリ

 

😊遊び人の『ティミー』
ティミー型は、ゲームを通じて楽しい体験を求めるプレイヤーです。
羊とは関係ありません。
ここでいう楽しさには興奮とか爽快感、協力感など色々ありますが、
つまりは、勝敗とは別のところで感情の高ぶりを求めます。
同じ実力でぶつかりあうギリギリの緊張感や、
超必殺技みたいな高威力の切り札で逆転するロマンとか、
あえてランダムに身をゆだねようとする気まぐれさなどが特徴です。

一方、ティミーにとっては、毎回同じ展開の淡々とした進行をする
ゲームはつまらなくなったり飽きやすくなります。
新鮮な体験、感情を動かしてくれる要素をティミーは評価します。

😐表現者『ジョニー』
ジョニー型は、ゲームを使って独創性を発揮したいプレイヤーです。
コインと居合いで馬鹿強い格闘ゲームのキャラとは関係ありません。
ゲームは自分の考えを表現して披露するためのキャンバスで、
ユニークだと評価されることがこのタイプにとっては大切です。
何か作ったり、ルールを研究したり、珍しい攻略法を生み出すことを好み、
その結果、縛りプレイを完遂したり、変わった斬新なカードコンボを
提案したり、非常識な攻略を発見するなどの行動に出るのが特徴です。

逆にジョニーにとって、自由度が低かったり、決まり手が分かっていて
選択がないゲームはつまらなくなりがちです。
創意工夫をこらす余地があるかが、ジョニーにとっては大切です。

😠戦士『スパイク』
スパイク型は、自分の能力を高め、実力を証明するためにゲームを
プレイしている、対戦ゲームで言えば勝ちたいプレイヤーです👑
スペースカウボーイとは関係ありません。
勝ちたいのはどのプレイヤーでも当然では、と思うかもしれませんが、
意外と上記のカテゴリ2種は勝敗よりも違うところに目的があり、
ある種「エンジョイ勢」という言い方もできるかもしれません。
その意味では一番「ガチ」っぽいのがスパイクということになります。

そしてスパイクにとっては、目標と達成が決まらないゲームではプレイ
意欲を高めることが難しくなります。
どちらかというと対戦相手や挑戦する課題が明確なゲームを好みます。

───────────────────────────────

🌟どこで遊んでいるの?

 

『マジック・ザ・ギャザリング』に代表される対戦トレーディング
カードゲームの楽しみ方なんて、そんなに種類はないと思われるかも
しれませんが、こうして見てみると結構、色んな楽しみ方の方向性が
あったりするようです。
なお、このカテゴリの分類は対戦ゲームであればどれでも応用が利く
ものと個人的に思っています(逆に、対戦ジャンルでないゲームだと
ちょっと適用が難しい感じですな…)

とはいえ、ここまでに3つのカテゴリを紹介しておいて何ですが、
プレイヤーをこのようにはっきり割り切ることはできません
プレイヤーの心理とは、血液型のように仕切られた形をしておらず、
各カテゴリの要素が混在して形成されているからです。
だから図解した場合、下図の感じに分布図のどこかに入るという感じです。

そんなわけで、対戦ゲームの面白さを誰かと論じたり、紹介をしたい時
自分と相手とがお互い、この分布図のどこにいてプレイしているのか
考えてみることをお勧めします。
例えば、私は「ジョニー」と「ティミー」の間にいるわけですが
相手がそのどちらかの価値観を持っているようであれば、
そこを共通基盤に話を膨らませることができるでしょう。
相手が「ジョニー」ならユニークな戦い方やコンボを話題の中心にしたり、
相手が「ティミー」なら、そのゲームの逆転ロマンの楽しさを語ったりと
ゲームのどの部分を要点に切り出すか考えるわけです。

ゲームの面白さについては、いまだ多くが共通言語化されておらず
それでなくても主観的な考えの混じりやすい話題です。
だからこそ、自分自身と、話す相手とのことだけでも相互に理解しあって
準備を済ませて、そのゲームの内容について納得しあって話しあえるよう
日ごろから心がけておきたいものです。
まぁ、これは何もゲームを語らうことだけの話でもないのでしょうが☝

🌟参照記事:デザインのための言葉 http://iwasgame.tumblr.com/post/80059856981


やらないことの選択も

$
0
0

ここ最近、寒暖の差が激しい日々が続いております。
暖かい方は大歓迎ですが、寒さは辛し!😭
花粉が来る😷としても、春が恋しいものです。
こんにちは、ハラです。

唐突ですが、私。
人生初の積みゲーを体験しております。

人生初と言っても、ゲームを凄く早くクリアするからこれまで起きなかった・・・・
とかではなく、プレイする余裕がある時にだけ買っていただけだったりします🎮

今回は、とあるキャンペーンに誘惑され、時間が無いのに買ってしまいました。
どうしよう!😖

 

携帯出来るゲームであれば、電車に乗っている時間なども使えます。
しかし、今回のは据え置き機のゲーム。

睡眠時間を減らし、活動時間を増やすか!
と思うも、あまり減らすと体を壊して、業務に支障が出ます。😪

今ある活動時間の中で何とかするしかない!

 

現在、やるべき事は、モリモリと積まれております。
だからこそ、買ったゲームをプレイする時間が無い訳で

そのため、手順を考えて効率化を・・・・、真の力を解放してスピードアップを💪・・・・
は、簡単に出来ない予感がするので、今回は諦めます。

 

ここは、今も行っている事の中から、敢えて「やらない」ようにする方向を考えてみたい!💡

絶対に要る訳ではない、その中から選ぶなら・・・

1.食事の支度をゼロにして外食に!
2.掃除をしない!
3.スマホを使うのを止める!
4.テレビを見ない!

何かダメな方向へ進んでる気がするので、後で考え直します!!😱

 

さて、私のダメダメな発想はさておき。
学生の皆さん、この時期は、既に山場を迎えている方、山場が近いのでその準備をされている方、大勢居られると思います。

山場には、「やらなければならない事」「やった方が良い事」が、沢山出てくると思います。
でも、待って下さい。

それらは全部出来そうですか?

多過ぎると感じるならば、一度立ち止まり「やらなくても良い事」が混ざってないかを考えてみるのも手だと思います。
焦って選んだ中には、本当はやらなくても良い事が混ざっているかもしれません。

やるべき事が絞られて来れば、より集中して立ち向かえると思います。

 

そうして、山場を乗り越えた先、ヘキサドライブの門を叩いて頂けると嬉しいなぁ・・・

 

 

よし、やはり掃除は諦めよう!😁

大雪に注意

$
0
0

こんにちは、ハットリです。
中断していたスタートレックディスカバリーが再開して
狂喜乱舞中です。

SFネタを書こうと思っていたら、本日は
雪が積もりそうで、スタッフは帰宅推奨になってます。


雪がかなり降っています。
明日の出社は混乱の予感。
大急ぎでこのブログを書いてます。

年に1、2回は大雪イベントが発生しますが
プロジェクトが忙しくて深夜まで残ったら
帰れなくなったことがありました。
エアコンが切れて凍えそうになったことも。😣

皆さんも降雪の際は早めの帰宅を。
転倒に気をつけて!

体験してみた 【和菓子編】

$
0
0

こんにちは。戌年になってひと月経つというのに、生コーギーに未だ遭遇していないラキアです。
もう代々木公園わんわんカーニバルに行きます…

 

今回は…一度やってみたかった事の一つ、【練りきり】の和菓子づくり体験に行ってきました。

※【練りきり】…白餡に “つなぎ” (牛皮など)を加えて練った生菓子。

 

↑先生のお手本。戌年という事で、柴犬の練りきりを 3個 作ります。

かなりの強敵…こんな可愛く作れるのか俺…

 

 

 

材料はこんな感じ。

①こしあんを白い練りきりで包んで…

②オレンジの練りきりを伸ばして①に被せて…

③スプーンの先を耳の位置で刺して押し伸ばして…

④眼・鼻を付ければ完成~

※衛生面の関係で、各工程の画像は有りません…

 

 

簡単そうに見えますが…

乾燥が大敵なので、手がカッサカサだったり、モタモタしてると、犬じゃなくなる。

常に手を湿らせていないと、生地が破れたりして綺麗に作れないそうです…

1個目。犬…なのかな……狐っぽく見えなくも無い…ムムム

生地破けて、修正作業に時間割きすぎました…

 

 

もう同じ失敗はしない。

2、3個目。柴犬でなく、コーギーに寄せてみたが……愛が足りなかった。

あっという間に終わってしまいました。出来栄えは微妙でしたが、味は最高。

練りきり、奥深くて面白い。また近いうちにチャレンジ!

 

レイヤーインスタンス

$
0
0

どうも。。内村です。

今回はSubstancePainter 2017.4で追加されたレイヤーインスタンスを使ってみました。

アップデート自体は去年の11月末だったので、やや情報が遅い感はありますが気にせずやります…

機能としては…

メッシュに複数のTexture setがある場合にsetをまたいでレイヤーを共有できる機能です。

 

メッシュはMeetMatを使わせていただきます。

Texture setは01_Head   02_Body   03_Baseの3つです。

ではまず何もテクスチャがない状態から…

 

01_HeadのTexture setにテクスチャリング…

 

共有したいレイヤーの上で右クリックからInstantiate across Texture setsを選択

※フォルダでも可能です。フォルダの場合は含まれているレイヤーすべてがインスタンス対象になります。今回はフォルダからインスタンス化

 

インスタンスコピー先のTexture setを選択して…

 

適応完了!すごく便利ですねぇ…

 

 

特に意味はないけど、Irayでレンダリング…

では今回はこのへんで…ノシ

光陰矢の如し

$
0
0

こんにちは。テララです。

寒い日が続きますね。

道には先日の余韻、灰色に汚れた雪が残り、さらに冬を痛感させてきます。

日々が過ぎるのは早いもので、

Ficustone projectとしては発足してから二度目の冬を迎えたことになります。

 

※Ficustone projectとは?

ヘキサドライブの小規模オリジナルコンテンツブランドです。

(詳細は”こちら”からどうぞ。)

 

Ficustoneは2018年に3作目のリリースを予定していますが、

実はこの3作目の開発メンバーは1作目とは微妙に顔ぶれが異なっていたりします。

1作目、2作目、3作目と、チーム内部でじょじょにメンバーも変化しているのですね。

そういったところからも月日の経過を感じます。

(3作目のティザーサイトは”こちら”からどうぞ。)

 

また、そろそろ就活に向けて学生さんがそわそわし始める時期でもありますよね。

学生さんを見かけたり、学生さんとお話する機会も増えました。

(リクルート関連の情報は”こちら”からどうぞ。)

 

あれ?このブログ、挨拶と宣伝で終わろうとしている……?

そうです、挨拶と宣伝で終わろうとしています。決してネタがなさすぎて困ったわけではないですよ。

 

ここまできたら、最後にもう一個、宣伝を追加しちゃいましょう。

Ficustoneとは別プロジェクトですが……

先月リリースした「MakeS -おはよう、私のセイ-」も、オススメですので、

ぜひチェックしてみてください。

素敵な彼が、きっとあなたの心を暖めてくれることでしょう。

(セイくんについては”こちら”からどうぞ。)

 

以上、テララでした。

早上がりDay -登頂編-

$
0
0

ひつじのショーンの劇場版2作目の公開はいつになるのでしょうか……

情報を待っているアンディです

 

本日1/26(金)はヘキサドライブの「早上がりDay」です。

「早上がりDay」とは、プレミアムな金曜日に近い取り組みとして、

ほぼ2ヶ月に1度、15時定時で業務を終了し、

社員同士の交流や、普段行えないことをする機会としております!

 

 

本日、東京オフィスでは近くのボルダリングジムへ向かい、

ボルダリング会も開催する予定です

 

不定期開催されているボルダリング会は

ほぼ初心者のみですが、わいわいほんわかやっています。

また、ボルダリング会以外にもスポーツ系の集まりや、

ダーツ会なども開催されることもあります!

 

弊社の雰囲気が気になる方は、

リクルートサイトもご覧ください!

ではまた!

敵とか武器とかアイテムとか その4

$
0
0

先日、人生で初めてのスノーボードに行ったら、

偶然取材できていたラジオ局の人に

「今ラジオの企画で、最近の若者に取材をしてるんですが、大学生ですか?」

って話しかけられました。

溢れ出る若さが抑えきれないようです。

こんにちは、だっちです。

(雪ではしゃいで小さい雪だるまを作っていたから話しかけられたわけじゃないはず!

 

 

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

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

 

前回は「クラスファイルの生成」を行うところまで出来たので、

今回は予定通り「データファイルの書き出し」を行っていきたいと思います。

 

ファイルの形式は、とりあえずXML形式で書き出していこうと思います。

前回までのプログラムを改良して、データファイルを書き出してきます。

 

データはこんな感じで用意してみました。

コードは長くなってしまったので一番下に載せておきます。

 

実際に生成されたXMLファイルはこんな感じです。

これで、「データファイルを書き出す」事ができましたね!

 

ただ、単純にXMLファイルを書き出すだけならExcel標準の機能でも行うことが出来ますし、

前回生成したクラスファイルも活用していなくてせっかく実装した恩恵が少ないですね。

 

次回からはもう少し便利になるように拡張していこうと思います。

 

ではでは~

 

 

以下ソースコードです。

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

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 class Data
		{
			public List		records		{ get; }		//!< レコード

			// @brief	コンストラクタ
			// @param	[in]	records	レコード
			public Data(List records)
			{
				this.records = records;
			}
		}

		// データ1レコード
		private class Record
		{
			public List		values		{ get; }		//!< 値

			// @brief	コンストラクタ
			// @param	[in]	values	値
			public Record(List values)
			{
				this.values = values;
			}
		}

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

		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;				//!< メンバ変数最大許容数

		private const int		DATA_START_ROW				= 1;				//!< データ開始業
		private const int		DATA_START_CELL				= 1;				//!< データ開始列
		private const int		DATA_MAX					= 1024;				//!< データ最大許容数


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

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

			Data data = null;
			readExcelFile(srcPath, classInfo, out data);

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

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

		// @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();
			var records				= 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]	srcPath		読み込みファイル名
		// @param	[in]	classInfo	クラス情報
		// @param	[out]	data		データ
		// @return	なし
		private static void readExcelFile(string srcPath, ClassInfo classInfo, out Data data)
		{
			data = null;

			// クラス情報有効性チェック
			if( classInfo == null ||
				classInfo.memberVariableInfos == null ||
				classInfo.memberVariableInfos.Count <= 0 ) {
				Console.WriteLine("\"classInfo\" is invalid.");
				return;
			}

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

			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 == DATA_SHEET_NAME ) {
						Console.WriteLine();

						Console.WriteLine("Records");

						var valueMax = classInfo.memberVariableInfos.Count;
						var records = new List();
						// レコード数分ループ
						for( int rowIndex = DATA_START_ROW, recordCount = 0; recordCount < DATA_MAX; ++rowIndex, ++recordCount ) {
							var values = new List();
							var isEndRead = false;
							// メンバ変数分ループ
							for( int cellIndex = DATA_START_CELL, valueCount = 0; valueCount < valueMax; ++cellIndex, ++valueCount ) {
								// セルの内容取得
								var cellStr = getCellValue(sheet, rowIndex, cellIndex);

								// 1セル目が空白なら終了扱い
								if( valueCount == 0 &&
									string.IsNullOrEmpty(cellStr) ) {
									isEndRead = true;
									break;
								}

								Console.Write(cellStr + ", ");
								values.Add(cellStr);
							}
							if( isEndRead ) break;

							Console.WriteLine();
							records.Add(new Record(values));
						}

						data = new Data(records);
					}
				}
			}
		}

		// @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]	outDirectory	書き出し先ディレクトリ
		// @param	[in]	classInfo		クラス情報
		// @param	[in]	data			データ
		// @return	なし
		private static void writeDataFile(string outDirectory, ClassInfo classInfo, Data data)
		{
			// 入力情報が不正なら終了
			if( string.IsNullOrWhiteSpace(outDirectory) ) {
				Console.WriteLine("outDirectory is invalid....");
				return;
			}
			if( classInfo == null ||
				classInfo.memberVariableInfos == null ||
				classInfo.memberVariableInfos.Count <= 0 ) {
				Console.WriteLine("classInfo is invalid...");
				return;
			}
			if( data == null ||
				data.records == null ||
				data.records.Count <= 0 ) {
				Console.WriteLine("data is invalid...");
				return;
			}

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

			// ドキュメント作成
			var xmlDocument = new XmlDocument();

			// 定義、ルートノードを作成してドキュメントに追加
			var xmlDeclaration = xmlDocument.CreateXmlDeclaration("1.0", "UTF-8", null);
			xmlDocument.AppendChild(xmlDeclaration);
			var xmlRoot = xmlDocument.CreateElement("root");
			xmlDocument.AppendChild(xmlRoot);

			var memberVariableInfos = classInfo.memberVariableInfos.ToArray();

			// レコードを全てルートに追加
			foreach( var record in data.records ) {
				var xmlRecord = xmlDocument.CreateElement(classInfo.name);

				var values = record.values.ToArray();

				// 値を全てレコードに追加
				for( var i = 0; i < memberVariableInfos.Length && i < values.Length; ++i ) {
					var xmlValue = xmlDocument.CreateElement(memberVariableInfos[i].name);
					xmlValue.InnerText = values[i];
					xmlRecord.AppendChild(xmlValue);
				}

				// ルートに追加
				xmlRoot.AppendChild(xmlRecord);
			}

			// ファイルに保存
			using( var sw = new StreamWriter(outDirectory + classInfo.name + ".xml", false, new UTF8Encoding(true)) ) {
				xmlDocument.Save(sw);
			}
		}

		// @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();
		}
	}
}

エコなリバースフットリグ

$
0
0

今年は、平昌オリンピックやサッカーワールドカップなどスポーツの年になりそうですね。
寒さの折いかがお過ごしでしょうか、おおみや(む)です。

今日はリバースフットリグを作ってみようと思います。
私はあまりリバースフットを好まないのですが、人によってはリバースフットの
粘りけのあるオペレーションを好む人もいます。

どちらも大事ということで、私のリグの上に下駄を履かせるような感じの
即席でエコなリバースフットリグをご紹介しようと思います。

以前、紹介したリグ。

同様の足のリグ。
ポールベクター、足のIK制御兼くるぶしの回転リグ、指先回転リグの簡素なものです。
これに下駄を履かせます。

1.toe2ジョイントの位置にrv_foot_rig(ロケーター)、toeジョイントの位置にrv_toe_rig(ロケーター)を回転を合わせて配置し、
2.rv_toe_rigをrv_foot_rigの子供にします。
これで下駄の準備完了。

3.rv_foot_rigを主にして、foot_rigにoffsetアリでペアレントコンストレイン。
4.toe_rigのローテートにそれぞれ、エクスプレッションを入れます。

rotateXのエクスプレッションへ
toe_rig.rotateX = rv_toe_rig.rotateX *-1;

rotateYのエクスプレッションへ
toe_rig.rotateY = rv_toe_rig.rotateY *-1;

rotateZのエクスプレッションへ
toe_rig.rotateZ = rv_toe_rig.rotateZ *-1;

ハイパーグラフはこんな感じになります。

完成です。

パントマイムのクリックやフィックスポイント、メカニズムを知っていれば、リバースフットが
無くとも接地感のある動きは良い感じに作れたりしますが、それはまたの機会に。

それではまた。

脱出!

$
0
0

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

今回は、ボードゲームのお話。

年末に田舎に帰ったときに暇つぶしに家族で遊べるかなと思って、
こんなの買ってみました。

 

360度恐怖体験 脱出!おばけ屋敷ゲーム!!

ボードゲーム買ったのって小学生以来かも。昔はパーティジョイシリーズ色々持ってたなぁ・・・

ちなみにこの「360度恐怖体験 脱出!お化け屋敷ゲーム」、
1980年に発売された「おばけ屋敷げーむ」の進化版とのこと。

簡単にゲーム内容を説明すると、
お化け屋敷に閉じ込められたプレイヤーは様々なおばけと対決、仲間にして、
他のプレイヤーと協力しながら4体の中ボスお化け、ラスボスおばけを倒して脱出するというもの。

うちには6歳の子供がいるのですが、
勝ち負けのある遊びをすると負けたときにグズっちゃったりするんですよね。まぁ小さい子にはありがちですね。
ところがこのゲーム、プレイヤー全員vsおばけという作りなので、
プレイヤー同士の勝ち負けはなく、
プレイヤー全員で脱出するか、全員で負けるかなので・・・子供がグズらない!

また、「360度恐怖体験」ってことでスマホを使用した遊びも。
”おばけ探しマス”にとまると”おばけカード”を引くのですが、
カードを引く代わりにスマホの専用アプリを使用しておばけを探します。
おどろおどろしい音楽の中、360度見回しながらお化けを探す遊びに子供は大喜び!!

おかげで大ハマりして、昨日も寝る前にやらされる羽目に。
いや、おもしろいんだけど、さすがに何度も何度も・・・

とはいえ久々のアナログゲーム、
とても面白くて家族で気軽に楽しめました。
みなさんもデジタルばかりじゃなく、たまにはアナログなゲームもいかが?

西遊記 ヒーロー・イズ・バック

$
0
0

こんにちは、関東地方では本日も雪が降りそうで
帰りが心配なグリフォンです🐥

先日、久々に映画を見てきました❕

CGアニメーション『西遊記 ヒーロー・イズ・バック』です❕❕

公式サイトはこちら↓↓
http://saiyuki-movie.jp/

2015年に中国で公開されて爆発的なヒットを記録したCGアニメーション作品で、
日本でも良く知られる孫悟空がリュウアーという少年と出会い、旅をする物語。

 

 

実はこの映画、ヘキサドライブでPlayStation®4ゲーム化プロジェクトが現在進行中です❗❗

映画のグラフィックそのままに、アクションゲームとして孫悟空が走り回っております😆

スタッフ採用も行っています↓↓
https://hexadrive.jp/recruit/news/183/

 

まだ絶賛公開中なので、週末に見に行ってみてはいかがでしょう。
それではまた✋

マイロくん

$
0
0

It is cold today.
What a day!

どーも、最近google翻訳にハマっているさとうです
英語って難しい!( very difficult!)

It is cold today.(寒いといえば・・)

最近・・一段と寒くなったせいか、家で飼っている猫(マイロくん)が布団に潜り込んでくるようになりました。
最初はかわいいなぁ っと思ってたのも束の間・・
今では 毛布 → 掛け布団 → 枕 と身の回りの物をすべて奪われてしまい
これでは飼い主(me!)が凍死するので、何か対策はないかと思い・・・猫専用のベットを作ることにしました!

ググったところ、IKEAで売られている「お人形用のベット」が猫にも使用できるとのことで
さっそく購入!




マイロが寝るか寝ないかはとりあえず置いといて・・作ってみようと思います



うーん、複雑な作りだ



説明書は見えないけど、とりあえずいってみよーう



バン!



ババン!



バババン!



できたー!



ドーン!!(so cute!)

ということで、今夜からはゆっくり寝れそうです
それではまた🔧 

Game2:Winter

$
0
0

こんばんは!モリタです!

先日は東京が雪で大変だった方も多いのではないでしょうか🌨
そんな中、はるかに寒いであろう❄ロシア❄の方で、
個人的に注目していたTVショウがあった(過去形なのは最後に・・)ので紹介させていただきます📺

番組名は『Game2:Winter』といいます。
https://www.youtube.com/watch?v=bBMV98Wu_rg

以下のURLに日本語で紹介された記事がありますが
http://gogotsu.com/archives/26638

ざっくり説明すると
————
・男女30人が熊や狼が生息するマイナス40度のシベリアにて約10ヶ月間サバイバルを行う。
・撮影クルーなどは同行せず定点カメラのみでの24時間撮影。
・生き残るためには何でもアリ‼
→殺人や暴行も可☠☠。但し「発覚」した場合、ロシアの法律に従って逮捕される🚨
・このサバイバルに生存した者は1億ルーブル(約2億円)の賞金。
・また視聴者は自分が応援するプレイヤー(参加者)に必要なアイテムを送ることが可能。
—————
近頃『PUBG』も流行っていますが、
こういったゲーム,マンガ的ルールの極端な部分を、現実でやるとは・・と、
色々考えさせられつつも、どんな内容になっていくんだろう・・と、かなり期待しておりました。

(こういったものは「自分ならどう生き延びよう・・」とか考えるのも面白いですよね!)

で、そういえばそろそろ放映されているかな?💓(日本語訳視聴サイトはどこだー)
と改めて確認したところ・・

 

・・偽番組・・と発覚してまして・・💔

 

http://gogotsu.com/archives/31440
(実際、上記から行ける応募サイトはリンク切れしています)

怖いもの見たさで超残念な気持ち半面「そりゃそうですよね・・」と思ったりも・・🙍

いずれにせよ、こういった色々な試みや波紋が、
何かのきっかけとして、新しい動向が生まれてくるのかもしれませんね‼

それでは、またですー✋

画像のリサイズを実装する(ニアレストネイバー編)

$
0
0

会社近くのコンビニでラーメンを食べているとき充実感があります。
ラーメン大好き某(なにがし)さんです。

ゲーム開発をする上で、1枚の画像を色んなサイズで書き出すことがあると思います。

これを手作業でやっていると、種類が増えるほど負担が無視できなくなります。
そして、元の画像に修正が発生すると、すべてリサイズし直しなので、大変なことになります。

こういった単純作業はコンピュータに任せてしまいましょう。
そうすれば時間の節約にもなるし、ヒューマンエラーも減らせます。

Photoshopならマクロを使うのも手ですが、
ここは、Unityで画像のリサイズ(拡縮)を実装していきます。

画像のリサイズ処理は、全ピクセルに対して色の補間をしてやることになります。
目的画像の1ピクセルを決めるとき、原画像から周辺のピクセルを取り出して補間します。
流れそのものは拡大でも縮小でも同じです。参照する範囲が違うだけ。

このとき使う補間手法によって、計算量や精度が大きく異なってきます。

最初は最も単純なニアレストネイバー法を例にして、リサイズの基礎を理解します。

リサイズ方法を理解するのにまず大事なことは、1ピクセルのサイズを意識することです。
これは図で見たほうが理解が早いです。

横に並んだ6ピクセルを10ピクセルに拡大するケースを想定して、各ピクセルの対応を整理してみます。

原画像sと目的画像dの間で、どのピクセルが対応するか考えるため、スケールを合わせてみました。
見ての通りそれぞれピクセル幅が異なるため、適切に座標変換を考えなくてはいけません。

ピクセルs[0]は原画像の座標系で、[0, 1)の範囲を取ります。s[0]の中心は0.5です。
同様に、s[1]は[1, 2)、末端のs[5]は[5, 6)の範囲を取ります。

目的画像に対しても同様に、ピクセルd[3]の中心は、目的画像の座標系で3.5となります。

これを原画像の座標系に変換すると、3.5 x (6 / 10) = 2.1です。これは原画像のピクセルs[2]に相当します。
つまり、d[3]に最も近い位置のピクセルはs[2]だということです。

一般化すると、d[i]にs[j]が対応するとき、スケールsとして、j = [(i + 0.5) / s]です。
[]は小数部の切り捨てを意味するガウス記号です。

実際には+0.5を省略して高速化することも多いでしょうが、
ピクセルの対応が変わってしまうのを嫌って今回は省略せずにいきます。

とりあえず、d[i]をs[j]で置き換えるような処理を全ピクセルに対して行うと単純な拡大ができそうです。

このように、原画像から最も近い位置の色で目的画像を生成する手法こそ、ニアレストネイバー法です。
ニアレストネイバー(Nearest Neighbor)とは、最近傍という意味です。

これを拡大に使うと、なんだかギザギザした画像になります。
ドット絵をぼやけさせずに拡大したいときなんかには都合がいいです。
補間によって新たな色が勝手に追加されないことが特徴といえますね。


↓弊社「魔法パスワード1111」のトルネードちゃんを1.5倍にスケール

整数倍でないとなかなかきつい仕上がりに・・・。

今度は逆に、10ピクセルを6ピクセルに縮小するケースを考えていきましょう。
先程の図の上下が反転します。

d[2]の中心は目的画像の座標系で2.5です。
これを原画像の座標系にすると、2.5 x (10 / 6) = 4.166…なので、s[4]に相当します。

d[2]をs[4]で置き換えるような操作を全ピクセルに対して行うと、
ニアレストネイバー法による縮小となります。

この手法による縮小は正直に言って、かなりイマイチです。
飛び飛びの位置のピクセルによる置き換えになるので仕方ないですね。
上の例では、s[1]、s[3]、s[6]、s[8]の情報を完全に失います。


↓弊社「魔法パスワード1111」のトルネードちゃんを0.5倍にスケール

アニメ的な塗りには大きな影響はないですが、線がきれいになりません。

ここから精度を上げていくには、
原画像から着目点の周辺ピクセルを考慮に入れる必要がありそうです。

ともかく、ニアレストネイバー法の説明は済んだので、実装例を示します。

using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Assertions;

public partial class Bitmap
{
	public uint width { get { return _width; } }
	public uint height { get { return _height; } }

	public uint count { get { return _width * _height; } }

	public Color[] pixels { get { return _pixels; } }

	public Color this[int x, int y]
	{
		get { return _pixels[_width * y + x]; }
		set { _pixels[_width * y + x] = value; }
	}

	public Color this[int index]
	{
		get { return _pixels[index]; }
		set { _pixels[index] = value; }
	}

	public Bitmap(Texture2D texture)
		: this(texture.GetPixels(), (uint)texture.width, (uint)texture.height)
	{
	}

	public Bitmap(uint width, uint height)
		: this(new Color[width * height], width, height)
	{
	}

	public Bitmap(Color[] pixels, uint width, uint height)
	{
		Assert.IsTrue(pixels.Length == width * height);

		_width = width;
		_height = height;
		_pixels = pixels;
	}

	public Bitmap copy()
	{
		var bitmap = new Bitmap(_width, _height);
		_pixels.CopyTo(bitmap._pixels, 0);
		return bitmap;
	}

	public Texture2D createTexture()
	{
		var texture = new Texture2D((int)_width, (int)_height, TextureFormat.RGBA32, false);
		texture.SetPixels(_pixels);
		texture.Apply();
		return texture;
	}

	public Color get(int x, int y)
	{
		x = clamp(x, 0, (int)_width - 1);
		y = clamp(y, 0, (int)_height - 1);
		return this[x, y];
	}

	private Bitmap resizeNearestNeighbor(uint width, uint height)
	{
		var bitmap = new Bitmap(width, height);

		// スケール
		var scaleX = (float)width / _width;
		var scaleY = (float)height / _height;

		// 単位を原画像の座標系に変換
		var unitX = 1.0f / scaleX;
		var unitY = 1.0f / scaleY;

		// 1ピクセル幅を考慮した座標の補正
		var correctionX = unitX * 0.5f;
		var correctionY = unitY * 0.5f;

		parallelFor(0, (int)bitmap._height, y => {
			for(int x = 0; x  max) return max;
		if(value < min) return min;
		return value;
	}

	private static void parallelFor(int start, int end, Action action)
	{
		var length = Math.Abs(end - start);
		if(length == 0) return;

		// start  end) {
			var tmp = start;
			start = end;
			end = tmp;
		}

		extendHandlePoolCapacity(length);

		// 並列タスクの生成と同期オブジェクトの取得
		var index = 0;
		var handles = new ManualResetEvent[length];
		for(int i = start; i  {
				action(arg);
				handle.Set();
			});

			handles[index++] = handle;
		}

		// 並列タスクの終了待ち
		foreach(var handle in handles) {
			handle.WaitOne();
			pushHandle(handle);
		}
	}

	private static void extendHandlePoolCapacity(int capacity)
	{
		if(_handlePool.Capacity < capacity) {
			_handlePool.Capacity = capacity;
		}
	}

	private static void pushHandle(ManualResetEvent handle)
	{
		_handlePool.Add(handle);
	}

	private static ManualResetEvent popHandle()
	{
		if(_handlePool.Count <= 0) {
			return new ManualResetEvent(false);
		}

		var lastIndex = _handlePool.Count - 1;
		var handle = _handlePool[lastIndex];
		_handlePool.RemoveAt(lastIndex);
		handle.Reset();
		return handle;
	}

	private static readonly List _handlePool = new List();

	private uint _width = 0u;
	private uint _height = 0u;

	private Color[] _pixels = null;
}

画像の入力については、読み取り許可されたTexture2Dからピクセルデータを取得できます。
取得したピクセルデータは、画像の左下を原点とした順序で得られることに注意です。
Texture2DからPNG/JPG変換が可能なので、これをファイルに保存すれば、出力も完了です。

このリサイズ処理は処理順に依存がないので、並列化により高速化しています。
Unity 5.6系までの.NET Frameworkが古くてParallel.Forが使えなかったので、
仕方なく自前で用意したのがparallelForメソッドです。

今回は画像処理部分を自前で実装しましたが、
ImageMagickなどのツールを実行するフロントエンドを作るのも当然アリです。

ただ、自分のケースでは、

 ・デザイナーPCに特殊な実行環境を入れさせたくなかった
 ・UnityのTexture2DならPSDファイルも入力ファイルとして扱える
 ・独自実装なら融通がきくので、他の処理との組み合わせがしやすい

上記の理由により、エディタ拡張から利用できるようにするのが使いやすいかな、と思いました。

次回からもっと精度の良い手法に乗り換えていきます。

ジオラマを作ろう

$
0
0

スカイリム始めました、ブリテンです。

さて、かねてよりヘキサプラモ部(東京)として、
おのおの個別に自由に活動しているわけですが、
最近はプラモそのものよりも、それらを配置する環境「ジオラマ」に興味が沸いています。
とはいえ、今まではジオラマよりも小さいビネットしか作ったことがなかったので、
よっしゃ1/48のジオラマ(だけ)作ったろうやんけ、と挑戦しました。

というわけでこちらのガレージキットを購入(秋葉原)

イメージ的にはこんな雰囲気にしたいと目標設定(GoT, Winterfell)

組み立て(パーツはあらかじめマジックリン等で洗っておきます)

パテ塗り(うちにはサーフェイサーを噴けるスペースがないのです)

下地塗り(元の素材の色はこんな色だろうな)

影になるであろう部分を塗る(デスクライトを活用しつつ)

で、本塗り(あれ?)

どうしてこんなことになってしまったのだろう・・・

あーあデジタルだったらCtrl+Zで戻せるのだろうな、と思いつつ、
やり直しです。

モノづくりにはこんなこともあるでしょう。
しかし、折れない心とクオリティへのこだわり
そして明確な目標への憧憬

があれば、きっとまた走りだせるはずです。
がんばれブリテン!きっとヘキサドライブ社内にちらほらいるプラモ部部員も
おのおの個別に自由に活動して、がんばってるぞ!


今日のMaya Python API 2.0

$
0
0

こんにちは

Houdini信者となった🎄ヨセミテ🎄です。ブログはMayaです

Houdiniは最高なので皆さんもHoudiniを使いましょう。

―――――――――――――――――――――――――――――――――――――――

今回はAPI 2.0でノードプラグインを作成してみます。
ノードプラグインをAPI 2.0で作成したことは現状、全くありません。
理解不十分な点が随所で見受けられるかも知れませんが温かいまなざしで見守って下さい。

テンプレートは前回同様、Autodesk公式のヘルプページで配布してくれています。
呼びだし時の挙動などをコメントでメモしつつ、とりあえず動くものを目標に作成します。

今回はMPxNodeクラスを継承したクラスを作成してみます。
継承元の分類などは公式ドキュメントに詳細が載っています

―――――――――――――――――――――――――――――――――――――――

以下スクリプトです。
内容としては接続された値に応じて接続先の移動値をsin波で出力するというもの。
現状では利用価値とかはほぼ皆無です

# coding: UTF-8
# ブログ用 OpenMayAPI2.0を利用したノード作成のテスト

import sys
import maya.api.OpenMaya as om

import math

def maya_useNewAPI():
    """
    このスクリプトはMayaプラグインの生産のためのもので、API2.0で利用しています。(英語翻訳)
    説明用の項目かな?
    """
    pass


# Plug-in information:
# サンプルのリンク先、Maya プラグイン エントリ/エグジット ポイント項目の各項目に詳細が記載
kPluginNodeName = 'blog_test_node'  # ノードの名前(1.ノード名参照)
kPluginNodeClassify = 'utility/general'  # ノードの分類(6.ノードの分類参照)
kPluginNodeId = om.MTypeId(0x07EFE)  # ノードごとのユニークな値、MTypeIDを作成。(2.ノードID参照)
# 内部開発用のプラグインは[x00000 - 0x7ffff#」の範囲を指定することを推奨している模様。社内で管理しましょう。

# 各アトリビュートに初期値として入れてる値
sampleDefaultValue = 1


##########################################################
# Plug-in
##########################################################
class BlogTestNode(om.MPxNode):
    # データハンドル設定用のMObject。computeメソッドにノード入出力先を受け渡すために利用されている
    in_attribute_offset_x = om.MObject()
    in_attribute_offset_y = om.MObject()
    in_attribute_offset_z = om.MObject()
    in_attribute_position_x = om.MObject()
    in_attribute_position_y = om.MObject()
    in_attribute_position_z = om.MObject()
    out_attribute_result_x = om.MObject()
    out_attribute_result_y = om.MObject()
    out_attribute_result_z = om.MObject()

    def __init__(self):
        """
        コンストラクタ
        """
        om.MPxNode.__init__(self)

    def compute(self, pPlug, pDataBlock):
        """
        :brief  計算処理用の関数(ノードの動作(Node Behavior参照)
                mayaはMPlugの接続ごとにcomputeを呼び出して、MPlugとMDataBlock(接続先とデータ内容?)を受け渡して
                ドキュメントに含まれている、「ダーティなプラグ」という表現は、
                多分だがデータの関連性が正しくないものをさしていると思われる
        :param  pPlug         一つのノードアトリビュートに関連した接続先
        :param  pDataBlock    計算用のデータを含むもの
        """
        # 出力が刺さっているときの処理をif分で追加していくみたい
        if (pPlug == BlogTestNode.out_attribute_result_x or
            pPlug == BlogTestNode.out_attribute_result_y or
            pPlug == BlogTestNode.out_attribute_result_z):
            # 各々のアトリビュートに対するデータハンドルの取得
            in_offset_data_handle_x = pDataBlock.inputValue(BlogTestNode.in_attribute_offset_x)
            in_offset_data_handle_y = pDataBlock.inputValue(BlogTestNode.in_attribute_offset_y)
            in_offset_data_handle_z = pDataBlock.inputValue(BlogTestNode.in_attribute_offset_z)
            in_position_data_handle_x = pDataBlock.inputValue(BlogTestNode.in_attribute_position_x)
            in_position_data_handle_y = pDataBlock.inputValue(BlogTestNode.in_attribute_position_y)
            in_position_data_handle_z = pDataBlock.inputValue(BlogTestNode.in_attribute_position_z)
            out_result_data_handle_x = pDataBlock.outputValue(BlogTestNode.out_attribute_result_x)
            out_result_data_handle_y = pDataBlock.outputValue(BlogTestNode.out_attribute_result_y)
            out_result_data_handle_z = pDataBlock.outputValue(BlogTestNode.out_attribute_result_z)

            # ======================= 計算式 =======================
            result_x = math.sin(in_offset_data_handle_x.asFloat() + in_position_data_handle_x.asFloat())
            result_y = math.sin(in_offset_data_handle_y.asFloat() + in_position_data_handle_y.asFloat())
            result_z = math.sin(in_offset_data_handle_z.asFloat() + in_position_data_handle_z.asFloat())
            # ======================================================

            # 出力値を設定
            out_result_data_handle_x.setFloat(result_x)
            out_result_data_handle_y.setFloat(result_y)
            out_result_data_handle_z.setFloat(result_z)

            # データハンドルが正常な値であることの記載(?) 計算が不要であることの説明らしい
            out_result_data_handle_x.setClean()
            out_result_data_handle_y.setClean()
            out_result_data_handle_z.setClean()

        else:
            # エラー時の処理。接続したのに接続先が見つからないときに呼び出されていた
            return om.kUnknownParameter


##########################################################
# プラグインの定義
# クラス外にあるのね
##########################################################
def nodeCreator():
    """
    createNode時に呼び出される場所
    """
    return BlogTestNode()


def nodeInitializer():
    """
    loadPluginを行った際に呼び出される模様
    入出力アトリビュートセット作成するらしいのでここで全入出力を用意する
    データハンドルの関連付けしなくてもここで定義しとけばとりあえずノード表面に入出力先として出てきた
    """
    print("called nodeInitializer define")

    # 同一のMFnNumericAttributeインスタンス内で複数の数値をもてるようだったので、
    # インスタンスは書き込み可能などの設定別に分ければいいのだと思う
    # …と思ったけどサンプルだとoutputでも使いまわしてた。とりあえず今回は設定ごとにインスタンスを作る
    # 使いまわしはミス発生元案件な気がするけど沢山インスタンス作ると処理遅くなっちゃったりするんだろうか
    MFnNAIns_input_offset = om.MFnNumericAttribute()
    MFnNAIns_input_position = om.MFnNumericAttribute()

    MFnNAIns_output_position = om.MFnNumericAttribute()

    # 定数
    default_value = 0.0

    # ==================================
    # 入力アトリビュート
    # ==================================
    # numericAttributeFn:ノード上への数値入力の出来るアトリビュート longName、shortName等を引数として与えている
    BlogTestNode.in_attribute_offset_x = MFnNAIns_input_offset.create('offset_x', 'ox',
                                                               om.MFnNumericData.kFloat, default_value)
    BlogTestNode.in_attribute_offset_y = MFnNAIns_input_offset.create('offset_y', 'oy',
                                                               om.MFnNumericData.kFloat, default_value)
    BlogTestNode.in_attribute_offset_z = MFnNAIns_input_offset.create('offset_z', 'oz',
                                                               om.MFnNumericData.kFloat, default_value)

    BlogTestNode.in_attribute_position_x = MFnNAIns_input_position.create('position_x', 'px',
                                                               om.MFnNumericData.kFloat, default_value)
    BlogTestNode.in_attribute_position_y = MFnNAIns_input_position.create('position_y', 'py',
                                                               om.MFnNumericData.kFloat, default_value)
    BlogTestNode.in_attribute_position_z = MFnNAIns_input_position.create('position_z', 'pz',
                                                               om.MFnNumericData.kFloat, default_value)

    # アトリビュートの属性設定
    # どうやら最後に作成したアトリビュートのみに適用されるみたい。インスタンス分けてない理由はこれか
    # MFnNAIns_input_offset.connectable = False # 綺麗にかけていないので一旦コメントアウト

    # 作成したアトリビュートを追加
    BlogTestNode.addAttribute(BlogTestNode.in_attribute_offset_x)
    BlogTestNode.addAttribute(BlogTestNode.in_attribute_offset_y)
    BlogTestNode.addAttribute(BlogTestNode.in_attribute_offset_z)
    BlogTestNode.addAttribute(BlogTestNode.in_attribute_position_x)
    BlogTestNode.addAttribute(BlogTestNode.in_attribute_position_y)
    BlogTestNode.addAttribute(BlogTestNode.in_attribute_position_z)

    # ==================================
    # OUTPUT NODE ATTRIBUTE(S)
    # 出力アトリビュート
    # ==================================
    BlogTestNode.out_attribute_result_x = MFnNAIns_output_position.create('result_x', 'rx', om.MFnNumericData.kFloat)
    MFnNAIns_output_position.writable = False  # writeble = Falseを設定しないと出力ノードとして認識してくれない模様

    BlogTestNode.out_attribute_result_y = MFnNAIns_output_position.create('result_y', 'ry', om.MFnNumericData.kFloat)
    MFnNAIns_output_position.writable = False  # 最後に作成したもののみに適用されるので何度も書いてる

    BlogTestNode.out_attribute_result_z = MFnNAIns_output_position.create('result_z', 'rz', om.MFnNumericData.kFloat)
    MFnNAIns_output_position.writable = False  # 最後に作成したもののみに適用されるので何度も書いてる

    # 作成したアトリビュートを追加
    BlogTestNode.addAttribute(BlogTestNode.out_attribute_result_x)
    BlogTestNode.addAttribute(BlogTestNode.out_attribute_result_y)
    BlogTestNode.addAttribute(BlogTestNode.out_attribute_result_z)

    # ==================================
    # アトリビュートの依存関係(?)
    # 恐らく計算処理の流れに関連する設定項目だと思う
    # ==================================
    BlogTestNode.attributeAffects(BlogTestNode.in_attribute_offset_x, BlogTestNode.out_attribute_result_x)
    BlogTestNode.attributeAffects(BlogTestNode.in_attribute_offset_y, BlogTestNode.out_attribute_result_y)
    BlogTestNode.attributeAffects(BlogTestNode.in_attribute_offset_z, BlogTestNode.out_attribute_result_z)

    BlogTestNode.attributeAffects(BlogTestNode.in_attribute_position_x, BlogTestNode.out_attribute_result_x)
    BlogTestNode.attributeAffects(BlogTestNode.in_attribute_position_y, BlogTestNode.out_attribute_result_y)
    BlogTestNode.attributeAffects(BlogTestNode.in_attribute_position_z, BlogTestNode.out_attribute_result_z)


# ここから下のイニシャライズ系はドキュメントで触れていなかったので、特殊な動作を必要としない現状は
# 触る必要性は無さそう

def initializePlugin(mobject):
    ''' Initialize the plug-in '''
    # プラグインの初期化
    mplugin = om.MFnPlugin(mobject)
    try:
        mplugin.registerNode(kPluginNodeName, kPluginNodeId, nodeCreator,
                             nodeInitializer, om.MPxNode.kDependNode, kPluginNodeClassify)
    except:
        sys.stderr.write('Failed to register node: ' + kPluginNodeName)
        raise


def uninitializePlugin(mobject):
    ''' Uninitializes the plug-in '''
    # プラグインの停止
    mplugin = om.MFnPlugin(mobject)
    try:
        mplugin.deregisterNode(kPluginNodeId)
    except:
        sys.stderr.write('Failed to deregister node: ' + kPluginNodeName)
        raise

 

―――――――――――――――――――――――――――――――――――――――

画像だけだと判りづらいですが、触っているオブジェクトを動かすと接続先も動きます。うん、まぁ一応使えそうです。

作ってみたの感想ですが、
・最初からアトリビュートをノードエディタに表示させる方法がわからなかった
・アトリビュートごとに設定を書く必要があるので現状、そのまま書いてるだけだとかなり冗長
・アトリビュートをまとめる方法を判らないと接続が大変
と現状、実用には程遠いポンコツな出来です。

―――――――――――――――――――――――――――――――――――――――

1回分でまとまる量でもなかったので次回は整理しながらの書き方を模索しつつ
ちゃんと実用性を考えたノードを作成したいと思います。
では。🎄

いざ南国 海の部

$
0
0

モンスターハンターワールド面白いですね。

一方で、元Riotや元BioWareなどの方々が集まって作っている、明らかにモンハン風のゲーム”Dauntless”はどんな感じに仕上がってくるのか楽しみにしている、おのってぃです。

(プレオープンしているので動画上がっていると思いますが、全く見ていません、、、)

 

さて今回は先日旅行にいったときのお話をしようかと思います。今回はスクロール多めでお送りします。

12月の最初に1週間ほど休みをいただいたので、日本から片道15時間弱?、メキシコに行ってきました。メキシコといってもいわゆるメキシコではなくリゾート地であるカンクンです。

拡大図の中のカリブ海に面している細い地域にいました。

そんな場所なのでまわりはどこを見ても

こんな感じで空も海もひらけていました。

写真はコスメル島という場所に向かうフェリー乗り場なのですが、

この海で1日、カンクン内のセノーテといわれる泉で1.5日潜ってきたので、今回はその模様をばしばし載せていきます。

まずは海です。

ちかくをエイが泳いでいたので

一緒に泳いだり、

海っぽい光景がたくさんあったりしました。(当日のログを見ないと魚の種類がわからない。。。)

当日は快晴でとても見晴らしが良くて、透明度100mくらいだという話でした。(参考までに伊豆のダイビングスポットで調子のいいときで15mくらいです。)

ちかくにウミガメもおり、他のチームは見られたらしいのですが、残念ながら僕達は見ることはできませんでした。

そんなこんなであっという間に海もぐりは終了。

翌日はセノーテ(泉)潜りです。また写真をだだっと載せていきます。

セノーテ名物 光のカーテン ①

セノーテ名物 光のカーテン ②

光越しに林が見えます。

セノーテは洞窟にある泉なので、潜っていると暗いところに突然明るいところがあらわれます。

暗いのと潜っているので、どうしてもぶれてしまいますね。。

最後、地上へあがるところですね。

 

今回うつしていませんが、セノーテダイビングは岩場に道を示すロープが張ってあるのですが、

真っ暗な中でライトを照らしながら進まないといけない箇所が多数あるため、とてもきょろきょろしてしまいます。

さらにロープを少し外れたところにはドクロマークの看板(有害物質あるよ的な)があったりしてなかなかスリリングでした。

ロープをけったり鍾乳石を蹴ったりしても顰蹙モノです。鍾乳石は100年で数cm伸びるらしいのですが、数メートル単位のものがコース脇ににょきにょきと。

僕の興味対象は①沈没船②地形③その他魚とか、という感じだったので、セノーテ(泉)ダイビングはとても有意義でした。日本では見られそうに無いものだらけでしたし。

そんなこんなで二日目も終了。雨季が明けたというのもありつつ、両日ともに快晴に恵まれてとてもリフレッシュしました。お薦めです。

 

基本的に潜ることを目標にカンクンに行ったので、今回ほど写真も内容も無いですが、次回は陸の部、マヤ遺跡編をお送りしようと思います。では。

TA と Maya Node Editor その2

$
0
0

こんにちは、Ritaro です。

こちらに記事を書くのはこれで5回目となります。
今回も、どうぞよろしくお願い致します。

TAにとって開発上の問題点を解くのに
技術的な引き出しを色々な持っておくことが必要、
について・・また少し紹介したいと思います。

前回の記事内容 https://hexadrive.jp/hexablog/others/19837/
に引き続き、Mayaの Node Editorまわりの話、その2です。

キャラクターセットアップに便利に使える手段の1つとして
Maya の Node Editor をご紹介しています。
今回もちょっとしたノードの組み合わせで、オブジェクトを
ワールド座標値で設定する方法をお見せしたいと思います。

また、このような Node Editor を使ってキャラクター制御用
の仕組みを組む時、良く使う計算のノードとして 外積 が出て来ます。
現在、ShaderFXを使ったリアルタイムシェーダーについてのビデオチュートリアルを
作成していますが、リアルタイムの光源処理で良く使う計算のノードは 内積 になります。
こちらの話は また別の機会に出来たら・・と考えています。

では、さっそく・・

ちょっと具体的な、面白い対処法、でもあったら『おお、使えるかも』
・・って思うようなものをまたご紹介していきましょう。

2)「Maya NodeEditor を使った コンストレイント」

Mayaを使ったキャラクターセットアップで良く使う
Mayaがデフォルトで用意している
“方向 コンストレイント (OrientConstraint)”  や
“ペアレント コンストレイント (ParentConstraint)” を
ノードエディタで見たことはありますでしょうか?
実はかなり複雑なノードのつながりになっており、これに手を加えて改善しよう
なんて思いたくないくらいの 接続状態になっております。

MayaNodeEditor_DefaultConstrtaint

こんな状態でなくても似たような方法、もしくはもっと分りやすくて簡単に組める、
もしかしたらより軽い処理で行えないか、と考えつくようなヒントをご紹介します。

 ■ 回転 コンストレイント

クォータニオン ノードを利用する方法

前回の階層下のノードをワールド座標で扱う方法として最後のノードから
親逆行列の値を 入力行列に再度差し込む と正しく値を計算出来ていました。

これと同じことを 回転をクォータニオンのノードを使って計算させることにも
利用すると、回転コンストレイント と同じ働きになります。

decomposeMatrix を 2つ その出力クォータニオンを
quatProd(クォータニオン プロダクション) を使って乗算し
最後に quatToEuler(クォータニオンからオイラーに) を使って
入力されたクォータニオン を オイラーの回転値に戻して 回転 につないであげます。

Quaternion_RotationConstrtaint

  ■ 移動 コンストレイント

decomposeMatrix と vectorProduct の ポイント行列積 ノードを利用する方法

移動値だけのコンストレイントは vectorProduct を利用することで
親逆行列の値を 入力することも出来るので、どんな階層構造でも対応出来ます。

たったこれだけで済むのでしたら NodeEditor の利用もチャレンジし易いのではないでしょうか。

VectorProduct_TranstationConstrtaint

 ■ 移動/回転/スケール コンストレイント

multMatrix と decomposeMatrix ノードを利用する方法

こちらも 親逆行列の値 入力行列に再度差し込むことでどんな階層下でも
正しい位置を計算します。
追加の値を この例では displayPoints の値からオフセット値も設定出来ます。

SRT_OffsetConstrtaint

 ■  Jointの回転 コンストレイントの考察

multiplyDivide や ジョイントの方向

Jointのコンストレイントは主に回転値を扱いたい としたら
“方向 コンストレイント (OrientConstraint)” 以外にどんな方法があるか
色々と考察するのも面白いです。
一番下の例は回転値をジョイントの方向からクォータニオンの反転から計算しています。

Joint_RotationConstrtaint

・・・という訳で、ワールド座標値を扱えると、階層構造の影響を受けずに
移動/回転/スケール を設定出来、コンストレイント と同等の制御が出来ました。

PS
日本語の説明矢印がView上に表示していますが、これは 作成>注釈…(annotation)
ていうオブジェクトで、日本語表示対応していますので、
シーンにあるものに説明を付けたい時に便利ですよ。矢印付きです。

次回も NodeEditor ネタ として ベクター(Vector) について出来たら
と思っております。

ではでは

 

生き物と折り

$
0
0

皆、小さな動物を捕獲用ネットで「ギャバッ」っとして「ヒュッ」ってやってるぅ???
どうも、タッキーです。
「モンスターハンター:ワールド」が発売されてから時間が経ちましたが、
ついつい小型動物を捕まえたくなってモンスターが狩れません

 

今回はそんな小型動物の「昆虫」と折り紙について触れてみます。

テントウムシなどの、飛翔するときに翅を開くタイプの虫は、普段翅を折りたたんで収納しています。
この翅を折りたたむときの折り目は決まっており、少ない折数で収納できる形が望ましいです。
テントウムシは外側にある硬い羽とお尻(正確には腹)を使って、「ういよういよ」と翅を手繰り寄せて翅を収納しているそうです。

サイエンスZERO「‘折り紙’大進化! 宇宙から医療まで」という回で紹介されておりました)

そのため、折り目は翅の収納には重要な要素となります。

翅を広げるとここまで大きくなりますが…

コンパクトに折られて収納されています

 

 

他にも、「カブトムシのツノの形は、幼虫の頭に折りたたまれて収納されている」
という説を唱えた論文もありました。

折り方は複雑なようですが、空気を送り込むだけでツノの形が出来上がるそうです。

論文はこちら↓です。興味のある方はどうぞ。
Complex furrows in a 2D epithelial sheet code the 3D structure of a beetle horn (Keisuke Matsuda,et al.)

 

たかが折り紙。されど折り紙。
「折り」という視点で自然界を見てみると面白い発見があるみたいです。

にらみ折り道からは脱線しましたが、
「折り紙の研究」は最先端を行くものなのかもしれませんね

ーという紹介でした。

ではノシ

手作り石鹸を作ろう!

$
0
0

三度の飯より鯵のたたき🐟東京デザイナーのエドモンドです😹
まだまだ寒い日が続きますが皆さんいかがお過ごしでしょうか?
私はというと休日は専ら中華スープに浮いてる油を合体させる事に注力しております。

さてさて今回はお絵かきでもなく羊毛フェルトでもなく石鹸を作りたいと思います!!!
以前東急ハンズで手作り石鹸キットなるものを見つけ、ずっと気になっていたので購入いたしました。これで宝石のような石鹸が簡単に作れるとのことなので試してみたいと思います✨✨✨

 

材料はこちら、今回は透明な石鹸と白い石鹸の2色を使います。
石鹸の着色は食用色素で行ないます。そして溶かして着色をするためのカップ、型にするためのトレーです。こちらは両方とも100均で購入いたしました。

 

まず最初に石鹸を溶かして着色するために、溶けやすい大きさに透明石鹸を切ります。
こちら包丁では無く、カッターなどでも簡単に切れますよ。小さいお子さんは危ないのでママと一緒にやってくださいね!

 

レンジで溶かすためにカップに分けました。
ここで一点注意なのですが、溶かした石鹸は大変熱くなるのでカップは耐熱のものでないとキケンです!
私は耐熱ではないカップでチンしてしまいレンジの中が現代アートになったのでお気をつけください

 

次に溶けた石鹸に食用色素で着色を行ないます。
食用色素は一度少量の水で溶かないと石鹸のなかでダマになるため、水で溶かしつつ色を調整し進めましょう。今回は、青、紫、ピンクの3色です。納得できる色になったら冷蔵庫で30分程度寝かせます。

 

~~~~~~~~~30分後~~~~~~~~~

 

そして固まったものがこちらになります。これをいい感じの大きさに細かく切ります。琥珀糖のようで少しおいしそうですね。

 

切ったカケラを固める用のトレーに青→紫→ピンクの順番に敷き詰めていきます。また石鹸同士の隙間を埋めるために水色と白の2色を用意しました。こちらを水色→白→水色の順番でトレーの中に注いでいきます。そしてまた冷蔵庫で30分寝かせます。

 

~~~~~~~~~30分後~~~~~~~~~

 

出来上がったものをトレーからはずしました。もう結構いい感じですね!
やや青の面積が広めになってしまいましたが、青→紫→ピンクの綺麗な層が出来ています。
白は今回は薄めずに入れてしまいましたが、もう少し透明と混ぜてやや薄めたほうが綺麗かもしれません。
仕上げに石っぽい形に切っていくと…

 

おおお~~~~~~ゲームのアイテムっぽい!
水属性攻撃+20%ダメージ、毎ターン少量のHP回復って感じですね!!

すごく海の力を秘めた石のよう!
深海都市ステージボス戦後、海を旅立つ主人公に王の息子の男の娘マーメイドちゃんが「これを持っていってください…どうか貴方の旅路に、海神のご加護があらんことを…」って泣きながら渡してくれる人魚の涙って名前のあれだ!!辛い!!!


ありがとう…ありがとう優しいマーメイドちゃん!!!笑っておくれ、君の笑顔が好きなんだ!!!私は君に人魚姫やオフィーリアみたいな悲しい結末は絶対絶対迎えさせないからね!!!!

 

 

さて、白い石鹸が余ったのでネコを作ります。

 

お弁当用シリコンカップの中にパンケーキに模様をつける方式で、手前に来るパーツから配置していきます。この時点ですでに可愛いのでこれは300%可愛くなること間違い無しです。瞳孔や口のほかにも、灰色の模様部分や青目の部分などを敷いた後に白で蓋をします。そして30分冷蔵庫で寝かします。

 

固まりました。シリコンのカップをはずします。

 

はい???
石鹸はあまり細かい模様などを作ることには向いていないようですね。
作業中に溶かした石鹸がどんどん固まっていくので成型がしにくいことや、(口元に謎のセクシーボクロ等が生まれてしまったり)
熱い石鹸を新しく流し込むことで細かいパーツが溶けてしまうことなどが要因として上げられます。なので決して私が猫を作ることが下手ではないことがわかりますQ.E.D.証明完了

 

では少しかわいそうではありますが
折角の石鹸なので石鹸としての役割を全うさせてあげようと思います!

_人人人人人人人人人人人人人人人人人人人人人人人人人人人人_
> エドモンドぢゃんゆ゛る゛してに゛ゃあ゛あ゛あん!!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

 


_人人人人人人人人人人人人人人人人人人_
> オラオラオラオラオラオラ!!!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^YY^Y^Y ̄
_人人人人人人人人人人人人人人人人人人人_
> よく泡立つに゛ゃあ゛あ゛あ゛ん!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^YY^Y ̄
_人人人人人人人人人人人_
> 石鹸だからね!!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

 

よく泡立って石鹸もご満悦のようです。

 

あ~~~石鹸作って!一人芝居して!とてもオシャレで有意義な時間だったな~~~!
皆さんもさくっと簡単に楽しく出来ちゃう石鹸作り、チャレンジしてみてはいかがでしょうか?
ではでは💨

Viewing all 731 articles
Browse latest View live