本チュートリアルの動画をYouTubeから試聴しながらの学習をお奨めしております。
このチュートリアルでは、PlayCanvasを使って3D Textを生成できるコンテンツを作っていきます。
以下のようなコンテンツが出来ます。
3Dテキストを1文字ずつモデリングするのは面倒ですが、テキストをコードから3Dメッシュに生成することでその手間を省きました。
参考動画: https://www.youtube.com/watch?v=jeLtkX4sFXA
参考フォーラム: https://forum.playcanvas.com/t/3d-text-geometry-generator/16690
今回はこれらを参考にして、テキストのフォントを参照しメッシュを作成するコンテンツを作ります。
目次
- Chapter01 - チュートリアル用のプロジェクトをForkする
- Chapter02 - 3Dテキストを表示してみよう
- Chapter03 - テキスト入力のUIを作成
- Chapter04 - テキスト入力するスクリプトを作成
- Chapter05 - 3D化したテキストを生成させる
- Chapter06 - UI調整とスマホ対応
Chapter01 - チュートリアル用のプロジェクトをForkする
チュートリアルを始めるためのプロジェクトを用意しています。これをForkします。
https://playcanvas.com/project/776758
Forkをクリックし、Projectの名前を決めてForkします。
Forkをクリックしてもページが動作しない場合は、リロードしてください。
Forkした自身のプロジェクトのページから、EDITORをクリックしてエディット画面へ進みます。
ForkしたProjectを一度LaunchするとBoxが隙間なく配置されているのが確認できます。
これは過去のチュートリアルから引用して使用しています。
参照: https://support.playcanvas.jp/hc/ja/articles/25188682394265
ASSETSを確認
事前に用意したアセットを確認していきます。
・Ammo
Ammo.jsというPlayCanvasで物理演算を用いる際にインポートされる物理エンジンを入れています。今回のチュートリアルではこのディレクトリは触れません。
・Fonts
今回のチュートリアルで使用するフォントのファイルが入っています。Arial、M+ 1M Regular、の2つのフォントを使用します。
・Scripts
今回のチュートリアルで使用するスクリプトファイルが入っています。
entityArray.js は背景のBoxを隙間なく配置するための処理を実行します。earcut.min.js、opentype.min.js はテキストフォントを3D化するためのライブラリです。
これらのライブラリは text-mesh.js で使用して3D化を実行しています。
この text-mesh.js は、以下のプロジェクトから引用しております。
参照 : https://github.com/playcanvas/engine/issues/2689
プロジェクト : https://playcanvas.com/project/755330/overview/3d-text
今回のメインとなるファイルは text-mesh.js になります。
これを中心に使用してチュートリアルを進めていきます。
Chapter02 - 3Dテキストを表示してみよう
まずは、3Dテキストを実際に表示させてみましょう。
使用するスクリプトは text-mesh.js です。
まずは、3Dテキストを表示させるためにエンティティを作成します。
「Mesh Text」という名前でエンティティを作成します。
ADD COMPONENT から SCRIPT を追加し、 text-mesh.js を設定します。
text-mesh.js にはスクリプト属性が用意されており、設定内容は以下のようになります。
- Font : フォント(.ttf)のバイナリーデータを設定
- Text : 表示するテキストを入力
- Alignment : 文字揃えを設定 (Left, Center, Right)
- Character Size : フォントサイズを設定
- Character Spacing : 字間を設定
- Kerning : フォントのカーニングを調整
- Depth : 3Dの奥行の長さを設定
- Max Curve Steps : フォントのアウトラインのベジェ曲線のパス分割数を設定
- Render Style : Solid か Wireframe レンダリングを設定
- Material : マテリアルを設定
これを以下のように設定してきます。
- Font : Fontsフォルダの mplus-1m-regular.ttf を設定
- Text : 任意のテキスト
- Alignment : Center
- Character Size : 0.5
- Character Spacing : 0
- Kerning : 1
- Depth : 0.25
- Max Curve Steps : 10
- Render Style : Solid
- Material : 新しくマテリアルを作成し追加
フォントはFontsフォルダに用意した mplus-1m-regular.ttf を使用します。
こちらの M+ というフォントの詳細は以下からご参照ください。
https://mplusfonts.github.io/
マテリアルはASSETSから新しくマテリアルを作成し設定します。マテリアルの詳細設定は任意です。
3Dテキストをカメラに対して正面を向くように表示させるために、Mesh Text のPositionとRotationを以下のように設定します。
- Position:
- x: 0 , y: 4, z: 0
- Rotation:
- x: -35, y: 45, z: 0
設定後、Launch から実行画面を見てみましょう。
以下の画像のようにテキストが表示されていればOKです。
日本語や他記号など、設定したフォントに対応していない場合は正しく表示されなくなりますので注意してください。
同封している arial.ttf で日本語を表示させようとすると以下の画像のように正しく表示されません。
表示が確認出来たら、次はテキストを入力するためのUIを作成していきます
Chapter03 - テキスト入力のUIを作成
テキストを入力できるUIを作成していきます。
Rootエンティティを選択してヒエラルキーから2D Screenのエンティティを追加します。
2D Screenのエンティティを作成したら、さらに Element Group を作成し、Button Element を作成します。
以下のような階層でエンティティを作成します。
作成した Button の名前を「Input Text」に変更し、
ELEMENTから Width: 500 , Height: 80 と設定します。
Launchで確認すると白い長方形が中央に表示されている状態です。
次に、Input Textエンティティの配下の TextエンティティのELEMENTの設定を変更します。
- Preset : Left Anchor & Pivot
- Auto Width: True
- Auto Height: True
- Color : 黒色に設定
上記に設定を変更します。
この時、Font に mplus-1m-regular.ttf が設定されていない場合は設定をしておいてください。
Text に任意のワードを入力すると、テキストが表示されると思います。しかし、日本語を入力すると表示されません。
これは設定したフォントに日本語の情報が設定されていないため表示されません。
テキストで日本語を入力させるために、ASSETSから該当するフォントを選択して日本語の設定を行いましょう。
日本語を設定させるためには、フォントの Characters という項目に使用する文字を入力し設定する必要があります。
これはWebフォントのサブセット化と同じようなモノと考えていただければわかりやすいと思います。
ひらがな、カタカナ、常用漢字など一般的なものを使用するなら以下の文字列を、この Characters に入力して PROCESS FONT をクリックします。
亜唖娃阿哀愛挨姶逢葵茜穐悪握渥旭葦芦鯵梓圧斡扱宛姐虻飴絢綾鮎或粟袷安庵按暗案闇鞍杏以伊位依偉囲夷委威尉惟意慰易椅為畏異移維緯胃萎衣謂違遺医井亥域育郁磯一壱溢逸稲茨芋鰯允印咽員因姻引飲淫胤蔭院陰隠韻吋右宇烏羽迂雨卯鵜窺丑碓臼渦嘘唄欝蔚鰻姥厩浦瓜閏噂云運雲荏餌叡営嬰影映曳栄永泳洩瑛盈穎頴英衛詠鋭液疫益駅悦謁越閲榎厭円園堰奄宴延怨掩援沿演炎焔煙燕猿縁艶苑薗遠鉛鴛塩於汚甥凹央奥往応押旺横欧殴王翁襖鴬鴎黄岡沖荻億屋憶臆桶牡乙俺卸恩温穏音下化仮何伽価佳加可嘉夏嫁家寡科暇果架歌河火珂禍禾稼箇花苛茄荷華菓蝦課嘩貨迦過霞蚊俄峨我牙画臥芽蛾賀雅餓駕介会解回塊壊廻快怪悔恢懐戒拐改魁晦械海灰界皆絵芥蟹開階貝凱劾外咳害崖慨概涯碍蓋街該鎧骸浬馨蛙垣柿蛎鈎劃嚇各廓拡撹格核殻獲確穫覚角赫較郭閣隔革学岳楽額顎掛笠樫橿梶鰍潟割喝恰括活渇滑葛褐轄且鰹叶椛樺鞄株兜竃蒲釜鎌噛鴨栢茅萱粥刈苅瓦乾侃冠寒刊勘勧巻喚堪姦完官寛干幹患感慣憾換敢柑桓棺款歓汗漢澗潅環甘監看竿管簡緩缶翰肝艦莞観諌貫還鑑間閑関陥韓館舘丸含岸巌玩癌眼岩翫贋雁頑顔願企伎危喜器基奇嬉寄岐希幾忌揮机旗既期棋棄機帰毅気汽畿祈季稀紀徽規記貴起軌輝飢騎鬼亀偽儀妓宜戯技擬欺犠疑祇義蟻誼議掬菊鞠吉吃喫桔橘詰砧杵黍却客脚虐逆丘久仇休及吸宮弓急救朽求汲泣灸球究窮笈級糾給旧牛去居巨拒拠挙渠虚許距鋸漁禦魚亨享京供侠僑兇競共凶協匡卿叫喬境峡強彊怯恐恭挟教橋況狂狭矯胸脅興蕎郷鏡響饗驚仰凝尭暁業局曲極玉桐粁僅勤均巾錦斤欣欽琴禁禽筋緊芹菌衿襟謹近金吟銀九倶句区狗玖矩苦躯駆駈駒具愚虞喰空偶寓遇隅串櫛釧屑屈掘窟沓靴轡窪熊隈粂栗繰桑鍬勲君薫訓群軍郡卦袈祁係傾刑兄啓圭珪型契形径恵慶慧憩掲携敬景桂渓畦稽系経継繋罫茎荊蛍計詣警軽頚鶏芸迎鯨劇戟撃激隙桁傑欠決潔穴結血訣月件倹倦健兼券剣喧圏堅嫌建憲懸拳捲検権牽犬献研硯絹県肩見謙賢軒遣鍵険顕験鹸元原厳幻弦減源玄現絃舷言諺限乎個古呼固姑孤己庫弧戸故枯湖狐糊袴股胡菰虎誇跨鈷雇顧鼓五互伍午呉吾娯後御悟梧檎瑚碁語誤護醐乞鯉交佼侯候倖光公功効勾厚口向后喉坑垢好孔孝宏工巧巷幸広庚康弘恒慌抗拘控攻昂晃更杭校梗構江洪浩港溝甲皇硬稿糠紅紘絞綱耕考肯肱腔膏航荒行衡講貢購郊酵鉱砿鋼閤降項香高鴻剛劫号合壕拷濠豪轟麹克刻告国穀酷鵠黒獄漉腰甑忽惚骨狛込此頃今困坤墾婚恨懇昏昆根梱混痕紺艮魂些佐叉唆嵯左差査沙瑳砂詐鎖裟坐座挫債催再最哉塞妻宰彩才採栽歳済災采犀砕砦祭斎細菜裁載際剤在材罪財冴坂阪堺榊肴咲崎埼碕鷺作削咋搾昨朔柵窄策索錯桜鮭笹匙冊刷察拶撮擦札殺薩雑皐鯖捌錆鮫皿晒三傘参山惨撒散桟燦珊産算纂蚕讃賛酸餐斬暫残仕仔伺使刺司史嗣四士始姉姿子屍市師志思指支孜斯施旨枝止死氏獅祉私糸紙紫肢脂至視詞詩試誌諮資賜雌飼歯事似侍児字寺慈持時次滋治爾璽痔磁示而耳自蒔辞汐鹿式識鴫竺軸宍雫七叱執失嫉室悉湿漆疾質実蔀篠偲柴芝屡蕊縞舎写射捨赦斜煮社紗者謝車遮蛇邪借勺尺杓灼爵酌釈錫若寂弱惹主取守手朱殊狩珠種腫趣酒首儒受呪寿授樹綬需囚収周宗就州修愁拾洲秀秋終繍習臭舟蒐衆襲讐蹴輯週酋酬集醜什住充十従戎柔汁渋獣縦重銃叔夙宿淑祝縮粛塾熟出術述俊峻春瞬竣舜駿准循旬楯殉淳準潤盾純巡遵醇順処初所暑曙渚庶緒署書薯藷諸助叙女序徐恕鋤除傷償勝匠升召哨商唱嘗奨妾娼宵将小少尚庄床廠彰承抄招掌捷昇昌昭晶松梢樟樵沼消渉湘焼焦照症省硝礁祥称章笑粧紹肖菖蒋蕉衝裳訟証詔詳象賞醤鉦鍾鐘障鞘上丈丞乗冗剰城場壌嬢常情擾条杖浄状畳穣蒸譲醸錠嘱埴飾拭植殖燭織職色触食蝕辱尻伸信侵唇娠寝審心慎振新晋森榛浸深申疹真神秦紳臣芯薪親診身辛進針震人仁刃塵壬尋甚尽腎訊迅陣靭笥諏須酢図厨逗吹垂帥推水炊睡粋翠衰遂酔錐錘随瑞髄崇嵩数枢趨雛据杉椙菅頗雀裾澄摺寸世瀬畝是凄制勢姓征性成政整星晴棲栖正清牲生盛精聖声製西誠誓請逝醒青静斉税脆隻席惜戚斥昔析石積籍績脊責赤跡蹟碩切拙接摂折設窃節説雪絶舌蝉仙先千占宣専尖川戦扇撰栓栴泉浅洗染潜煎煽旋穿箭線繊羨腺舛船薦詮賎践選遷銭銑閃鮮前善漸然全禅繕膳糎噌塑岨措曾曽楚狙疏疎礎祖租粗素組蘇訴阻遡鼠僧創双叢倉喪壮奏爽宋層匝惣想捜掃挿掻操早曹巣槍槽漕燥争痩相窓糟総綜聡草荘葬蒼藻装走送遭鎗霜騒像増憎臓蔵贈造促側則即息捉束測足速俗属賊族続卒袖其揃存孫尊損村遜他多太汰詑唾堕妥惰打柁舵楕陀駄騨体堆対耐岱帯待怠態戴替泰滞胎腿苔袋貸退逮隊黛鯛代台大第醍題鷹滝瀧卓啄宅托択拓沢濯琢託鐸濁諾茸凧蛸只叩但達辰奪脱巽竪辿棚谷狸鱈樽誰丹単嘆坦担探旦歎淡湛炭短端箪綻耽胆蛋誕鍛団壇弾断暖檀段男談値知地弛恥智池痴稚置致蜘遅馳築畜竹筑蓄逐秩窒茶嫡着中仲宙忠抽昼柱注虫衷註酎鋳駐樗瀦猪苧著貯丁兆凋喋寵帖帳庁弔張彫徴懲挑暢朝潮牒町眺聴脹腸蝶調諜超跳銚長頂鳥勅捗直朕沈珍賃鎮陳津墜椎槌追鎚痛通塚栂掴槻佃漬柘辻蔦綴鍔椿潰坪壷嬬紬爪吊釣鶴亭低停偵剃貞呈堤定帝底庭廷弟悌抵挺提梯汀碇禎程締艇訂諦蹄逓邸鄭釘鼎泥摘擢敵滴的笛適鏑溺哲徹撤轍迭鉄典填天展店添纏甜貼転顛点伝殿澱田電兎吐堵塗妬屠徒斗杜渡登菟賭途都鍍砥砺努度土奴怒倒党冬凍刀唐塔塘套宕島嶋悼投搭東桃梼棟盗淘湯涛灯燈当痘祷等答筒糖統到董蕩藤討謄豆踏逃透鐙陶頭騰闘働動同堂導憧撞洞瞳童胴萄道銅峠鴇匿得徳涜特督禿篤毒独読栃橡凸突椴届鳶苫寅酉瀞噸屯惇敦沌豚遁頓呑曇鈍奈那内乍凪薙謎灘捺鍋楢馴縄畷南楠軟難汝二尼弐迩匂賑肉虹廿日乳入如尿韮任妊忍認濡禰祢寧葱猫熱年念捻撚燃粘乃廼之埜嚢悩濃納能脳膿農覗蚤巴把播覇杷波派琶破婆罵芭馬俳廃拝排敗杯盃牌背肺輩配倍培媒梅楳煤狽買売賠陪這蝿秤矧萩伯剥博拍柏泊白箔粕舶薄迫曝漠爆縛莫駁麦函箱硲箸肇筈櫨幡肌畑畠八鉢溌発醗髪伐罰抜筏閥鳩噺塙蛤隼伴判半反叛帆搬斑板氾汎版犯班畔繁般藩販範釆煩頒飯挽晩番盤磐蕃蛮匪卑否妃庇彼悲扉批披斐比泌疲皮碑秘緋罷肥被誹費避非飛樋簸備尾微枇毘琵眉美鼻柊稗匹疋髭彦膝菱肘弼必畢筆逼桧姫媛紐百謬俵彪標氷漂瓢票表評豹廟描病秒苗錨鋲蒜蛭鰭品彬斌浜瀕貧賓頻敏瓶不付埠夫婦富冨布府怖扶敷斧普浮父符腐膚芙譜負賦赴阜附侮撫武舞葡蕪部封楓風葺蕗伏副復幅服福腹複覆淵弗払沸仏物鮒分吻噴墳憤扮焚奮粉糞紛雰文聞丙併兵塀幣平弊柄並蔽閉陛米頁僻壁癖碧別瞥蔑箆偏変片篇編辺返遍便勉娩弁鞭保舗鋪圃捕歩甫補輔穂募墓慕戊暮母簿菩倣俸包呆報奉宝峰峯崩庖抱捧放方朋法泡烹砲縫胞芳萌蓬蜂褒訪豊邦鋒飽鳳鵬乏亡傍剖坊妨帽忘忙房暴望某棒冒紡肪膨謀貌貿鉾防吠頬北僕卜墨撲朴牧睦穆釦勃没殆堀幌奔本翻凡盆摩磨魔麻埋妹昧枚毎哩槙幕膜枕鮪柾鱒桝亦俣又抹末沫迄侭繭麿万慢満漫蔓味未魅巳箕岬密蜜湊蓑稔脈妙粍民眠務夢無牟矛霧鵡椋婿娘冥名命明盟迷銘鳴姪牝滅免棉綿緬面麺摸模茂妄孟毛猛盲網耗蒙儲木黙目杢勿餅尤戻籾貰問悶紋門匁也冶夜爺耶野弥矢厄役約薬訳躍靖柳薮鑓愉愈油癒諭輸唯佑優勇友宥幽悠憂揖有柚湧涌猶猷由祐裕誘遊邑郵雄融夕予余与誉輿預傭幼妖容庸揚揺擁曜楊様洋溶熔用窯羊耀葉蓉要謡踊遥陽養慾抑欲沃浴翌翼淀羅螺裸来莱頼雷洛絡落酪乱卵嵐欄濫藍蘭覧利吏履李梨理璃痢裏裡里離陸律率立葎掠略劉流溜琉留硫粒隆竜龍侶慮旅虜了亮僚両凌寮料梁涼猟療瞭稜糧良諒遼量陵領力緑倫厘林淋燐琳臨輪隣鱗麟瑠塁涙累類令伶例冷励嶺怜玲礼苓鈴隷零霊麗齢暦歴列劣烈裂廉恋憐漣煉簾練聯蓮連錬呂魯櫓炉賂路露労婁廊弄朗楼榔浪漏牢狼篭老聾蝋郎六麓禄肋録論倭和話歪賄脇惑枠鷲亙亘鰐詫藁蕨椀湾碗腕丼傲刹哺喩嗅嘲彙恣惧慄憬拉摯曖楷毀璧瘍箋籠緻羞訃諧貪踪辣錮鬱ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶ 、。,.・:;?!゛゜´`¨^ ̄_ヽヾゝゞ〃仝々〆〇ー―‐/\~∥|…‥‘’“”()〔〕[]{}〈〉《》「」『』【】+-±×÷=≠<>≦≧∞∴♂♀°′″℃¥$¢£%#&*@§☆★○●◎◇◆□■△▲▽▼※〒→←↑↓〓∈∋⊆⊇⊂⊃∪∩∧∨¬⇒⇔∀∃∠⊥⌒。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !"#$%&'()-^\@[;:],./\=~|`{+*}<>?_
PROCESS FONTをクリックして、フォントが読み込み終えたらLaunchで日本語が表示されているか確認し、表示されていればOKです。
PROCESS FONTが完了するには時間がかかる可能性があります。
次は、実際に入力出来るようにスクリプトを作成していきます。
Chapter04 - テキスト入力するスクリプトを作成
ASSETSのScriptsフォルダに新しく inputValue.js を作成します。
作成後、以下のコードに書き換えます。
var InputValue = pc.createScript('inputValue');
// 3Dのテキストを作るEntityを設定
InputValue.attributes.add("meshText", {type:"entity"});
// Elementでテキストを表示しているEntity
InputValue.attributes.add("inputEntity", {type:"entity"});
InputValue.prototype.initialize = function() {
var self = this;
// テキストを入力するためのinput要素を追加
self.inputElement = document.createElement("input");
self.inputElement.type = 'text';
self.inputElement.style.position = "fixed";
self.inputElement.style.bottom = 0;
self.inputElement.style.right = 0;
self.inputElement.style.zIndex = 1;
self.inputElement.style.width = "50px";
self.inputElement.style.height = "50px";
self.inputElement.style.fontSize = "2rem";
self.inputElement.style.opacity = 0;
self.inputElement.style.cursor = "pointer";
document.body.appendChild(self.inputElement);
// クリックされたらinput要素にfocusする
self.entity.element.on(pc.EVENT_MOUSEDOWN, self.inputFocus, this);
// inputにfocusしてキー入力を処理
self.inputElement.addEventListener("keydown",function(){
self.inputVal();
});
self.inputElement.addEventListener("keyup",function(){
self.inputVal();
});
};
InputValue.prototype.inputFocus = function() {
// input要素にfocusさせる
var self = this;
window.setTimeout(function() {
self.inputElement.focus();
}.bind(self), 90);
};
InputValue.prototype.inputVal = function() {
// input要素に入力されたテキストを textMesh.js の createText() で3D化
if(this.inputEntity){
this.inputEntity.element.text = this.inputElement.value;
}
this.meshText.script.textMesh.__attributes.text = this.inputElement.value;
this.meshText.script.textMesh.createText();
};
このコードでは、DOMのinput要素を opacity:0; で非表示にし、Elementがクリックされたらinput要素にfocusされるようにイベントを作成してテキストが入力できるようになっています。
入力されたテキストは、Elementのテキストと3Dテキストにそれぞれ渡しており、3Dテキストは渡すと同時に this.meshText.script.textMesh.createText(); を実行して3Dテキストを作成させています。
書き換え後、エンティティの Input Text に ADD COMPONENTS から SCRIPT を追加し、スクリプト属性をそれぞれ設定していきます。
meshText には、3Dテキストを作成するエンティティを設定。
inputEntity には、Elementでテキストを表示しているエンティティを設定します。
設定後、Launchで動作を確認してみます。
日本語でもElementと3D Textが同様に入力が適用されていればOKです。
テキスト入力までできたので、UIのPositionを調整していきましょう。
ヒエラルキーから、2D ScreenエンティティのScreenコンポーネントの解像度を 1920 x 1080 に変更しましょう。
2D Screenエンティティ配下のGroupエンティティのPositionを y:300 に変更と、
ElementのPresetをBottom Anchorに変更します。
以下の画像のようになればOKです。
配置はそれぞれ調整していい塩梅を探しましょう。
テキストを入力するまで出来ましたので、次は3Dテキストを複製するようにしていきたいと思います。
Chapter05 - 3D化したテキストを生成させる
テキストを3D化させることは現状で出来ていますが、最初確認していたデモのように複製生成して背景を転げ落ちるようにします。
まずは、複製生成させるボタンを作成します。
ここではGroupエンティティ配下、Input Textエンティティと同じ階層に Buttonエンティティを作成します。
以下の画像の様に設定していきましょう。
- Name: Button Create
- Position:
- x: 0, y: -130 z: 0
- Element
- Width: 300
- Height: 80
- ColorやHoverなどの色の指定はお好みで変更しましょう。
Button Create配下のTextも同じように設定を変更しましょう
- Text: CREATE
- FontSize: 38ぐらい
- Colorはお好みで指定しましょう
- Outline Thicknessで太字を再現することも可能です
ASSETSのScriptsフォルダに新しく inputCreate.js を作成し、以下のコードに書き換えます。
var InputCreate = pc.createScript('inputCreate');
// 3Dのテキストを作るEntityを設定
InputCreate.attributes.add("meshText", {type: "entity"});
InputCreate.prototype.initialize = function() {
var self = this;
// 作成した3Dのテキストを入れる親Entityを作成
self.rigidGroup = new pc.Entity();
self.rigidGroup.name = "RigidGroup";
self.app.root.addChild(self.rigidGroup);
self.entity.element.on(pc.EVENT_MOUSEDOWN, self.buttonClick, this);
};
InputCreate.prototype.buttonClick = function() {
// input要素に入力されたテキストを textMesh.js の createText() で3D化
this.meshText.script.textMesh.createText(this.rigidGroup);
};
このコードでは、 RigidGroupというエンティティを作成し、text-mesh.js で作成した3DテキストをRigidGroupエンティティに追加する処理を実行しています。
ここでは this.meshText.script.textMesh.createText(this.rigidGroup); を実行しますが、引数に this.rigidGroup を追加しています。
この引数を活用するために、次は text-mesh.js の createText() のコードを一部書き換えていきます。
text-mesh.js を以下のコードに書き換えます。長いコードですが、ここで変更するのは createText() です。
/* jshint esversion: 6 */
const BEZIER_STEP_SIZE = 3;
const EPSILON = 1e-6;
const p = new pc.Vec2();
const p1 = new pc.Vec2();
const p2 = new pc.Vec2();
const vTemp1 = new pc.Vec2();
const vTemp2 = new pc.Vec2();
const vTemp3 = new pc.Vec2();
const vTemp4 = new pc.Vec2();
const vTemp5 = new pc.Vec2();
const vTemp6 = new pc.Vec2();
// Utility class for converting path commands into point data
class Polygon {
constructor () {
this.points = [];
this.children = [];
this.area = 0;
}
moveTo(x, y) {
this.points.push(new pc.Vec2(x, y));
}
lineTo(x, y) {
this.points.push(new pc.Vec2(x, y));
}
close() {
let cur = this.points[this.points.length - 1];
this.points.forEach(next => {
this.area += 0.5 * cur.cross(next);
cur = next;
});
}
conicTo(px, py, p1x, p1y, maxSteps = 10) {
p.set(px, py);
p1.set(p1x, p1y);
const p0 = this.points[this.points.length - 1];
const dist = p0.distance(p1) + p1.distance(p);
const steps = Math.max(2, Math.min(maxSteps, dist / BEZIER_STEP_SIZE));
for (let i = 1; i <= steps; i++) {
const t = i / steps;
vTemp1.lerp(p0, p1, t);
vTemp2.lerp(p1, p, t);
vTemp3.lerp(vTemp1, vTemp2, t);
this.points.push(vTemp3.clone());
}
}
cubicTo(px, py, p1x, p1y, p2x, p2y, maxSteps = 10) {
p.set(px, py);
p1.set(p1x, p1y);
p2.set(p2x, p2y);
const p0 = this.points[this.points.length - 1];
const dist = p0.distance(p1) + p1.distance(p2) + p2.distance(p);
const steps = Math.max(2, Math.min(maxSteps, dist / BEZIER_STEP_SIZE));
for (let i = 1; i <= steps; i++) {
const t = i / steps;
vTemp1.lerp(p0, p1, t);
vTemp2.lerp(p1, p2, t);
vTemp3.lerp(p2, p, t);
vTemp4.lerp(vTemp1, vTemp2, t);
vTemp5.lerp(vTemp2, vTemp3, t);
vTemp6.lerp(vTemp4, vTemp5, t);
this.points.push(vTemp6.clone());
}
}
inside(p) {
let count = 0, cur = this.points[this.points.length - 1];
this.points.forEach(next => {
const p0 = (cur.y < next.y ? cur : next);
const p1 = (cur.y < next.y ? next : cur);
if (p0.y < p.y + EPSILON && p1.y > p.y + EPSILON) {
if ((p1.x - p0.x) * (p.y - p0.y) > (p.x - p0.x) * (p1.y - p0.y)) {
count += 1;
}
}
cur = next;
});
return (count % 2) !== 0;
}
}
var TextMesh = pc.createScript('textMesh');
TextMesh.attributes.add('font', {
type: 'asset',
assetType: 'binary',
title: 'Font',
description: 'TTF file used as the basis for this 3D text'
});
TextMesh.attributes.add('text', {
type: 'string',
title: 'Text',
description: 'The text string to render as a 3D mesh'
});
TextMesh.attributes.add('alignment', {
type: 'number',
enum: [
{ 'Left': 0 },
{ 'Center': 1 },
{ 'Right': 2 }
],
title: 'Alignment',
description: 'Controls whether the text is centered or left or right justified'
});
TextMesh.attributes.add('characterSize', {
type: 'number',
default: 1,
title: 'Character Size',
description: 'The world space (maximum) height of each character'
});
TextMesh.attributes.add('characterSpacing', {
type: 'number',
min: 0,
default: 0,
title: 'Character Spacing',
description: 'Additional spacing between each character'
});
TextMesh.attributes.add('kerning', {
type: 'number',
min: 0,
max: 1,
default: 1,
title: 'Kerning',
description: 'Scales character pair kerning value so 0 is no kerning and 1 is full kerning'
});
TextMesh.attributes.add('depth', {
type: 'number',
default: 1,
title: 'Depth',
description: 'Depth of the extrusion applied to the text'
});
TextMesh.attributes.add('maxCurveSteps', {
type: 'number',
default: 10,
title: 'Max Curve Steps',
description: 'Maximum number of divisions applied to bezier based path in a font outline'
});
TextMesh.attributes.add('renderStyle', {
type: 'number',
enum: [
{ 'Solid': 0 },
{ 'Wireframe': 1 }
],
title: 'Render Style',
description: 'Controls whether the text is rendered as solid or wireframe'
});
TextMesh.attributes.add('material', {
type: 'asset',
assetType: 'material',
title: 'Material',
description: 'The material to apply to the 3D text mesh'
});
TextMesh.prototype.initialize = function() {
this.characters = [];
this.fontData = null;
if (this.font) {
this.fontData = opentype.parse(this.font.resource);
this.createText();
}
// Handle any and all attribute changes
this.on('attr', function(name, value, prev) {
if (value !== prev) {
if (name === 'font') {
if (this.font) {
this.fontData = opentype.parse(this.font.resource);
} else {
this.fontData = null;
this.destroyCharacters();
}
}
if (this.fontData) {
this.createText();
}
}
});
};
TextMesh.prototype.parseCommands = function (commands) {
// Convert all outlines for the character to polygons
var polygons = [];
commands.forEach(({type, x, y, x1, y1, x2, y2}) => {
switch (type) {
case 'M':
polygons.push(new Polygon());
polygons[polygons.length - 1].moveTo(x, y);
break;
case 'L':
polygons[polygons.length - 1].moveTo(x, y);
break;
case 'C':
polygons[polygons.length - 1].cubicTo(x, y, x1, y1, x2, y2, this.maxCurveSteps);
break;
case 'Q':
polygons[polygons.length - 1].conicTo(x, y, x1, y1, this.maxCurveSteps);
break;
case 'Z':
polygons[polygons.length - 1].close();
break;
}
});
// Sort polygons by descending area
polygons.sort((a, b) => Math.abs(b.area) - Math.abs(a.area));
// Classify polygons to find holes and their 'parents'
const root = [];
for (let i = 0; i < polygons.length; ++i) {
let parent = null;
for (let j = i - 1; j >= 0; --j) {
// A polygon is a hole if it is inside its parent and has different winding
if (polygons[j].inside(polygons[i].points[0]) && polygons[i].area * polygons[j].area < 0) {
parent = polygons[j];
break;
}
}
if (parent) {
parent.children.push(polygons[i]);
} else {
root.push(polygons[i]);
}
}
const totalPoints = polygons.reduce((sum, p) => sum + p.points.length, 0);
const vertexData = new Float32Array(totalPoints * 2);
let vertexCount = 0;
const indices = [];
function process(poly) {
// Construct input for earcut
const coords = [];
const holes = [];
poly.points.forEach(({x, y}) => coords.push(x, y));
poly.children.forEach(child => {
// Children's children are new, separate shapes
child.children.forEach(process);
holes.push(coords.length / 2);
child.points.forEach(({x, y}) => coords.push(x, y));
});
// Add vertex data
vertexData.set(coords, vertexCount * 2);
// Add index data
earcut(coords, holes).forEach(i => indices.push(i + vertexCount));
vertexCount += coords.length / 2;
}
root.forEach(process);
const scalar = this.characterSize / this.fontData.unitsPerEm;
// Generate front vertices
let vertices = [];
for (let p = 0; p < vertexData.length; p += 2) {
vertices.push(vertexData[p] * scalar, vertexData[p + 1] * scalar, this.depth);
}
// Generate back vertices
for (let p = 0; p < vertexData.length; p += 2) {
vertices.push(vertexData[p] * scalar, vertexData[p + 1] * scalar, 0);
}
// Generate back indices
const numIndices = indices.length;
for (let i = 0; i < numIndices; i += 3) {
indices.push(indices[i + 2] + vertexCount, indices[i + 1] + vertexCount, indices[i] + vertexCount);
}
// Generate sides
polygons.forEach(poly => {
for (let i = 0; i < poly.points.length - 1; i++) {
let base = vertices.length / 3;
let p1 = poly.points[i];
let p2 = poly.points[i + 1];
vertices.push(p1.x * scalar, p1.y * scalar, this.depth, p2.x * scalar, p2.y * scalar, this.depth,
p1.x * scalar, p1.y * scalar, 0, p2.x * scalar, p2.y * scalar, 0);
indices.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);
}
});
let normals = pc.calculateNormals(vertices, indices);
return { vertices, normals, indices };
};
TextMesh.prototype.calculateWidth = function () {
const font = this.fontData;
const scalar = this.characterSize / font.unitsPerEm;
let width = 0;
for (var i = 0; i < this.text.length; i++) {
let char = this.text.charAt(i);
width += font.charToGlyph(char).advanceWidth * scalar;
if (i < this.text.length - 1) {
width += this.characterSpacing;
var glyph = font.charToGlyph(char);
var nextGlyph = font.charToGlyph(this.text.charAt(i + 1));
width += font.getKerningValue(glyph, nextGlyph) * this.kerning * scalar;
}
}
return width;
};
TextMesh.prototype.destroyCharacters = function () {
// Destroy all existing characters
this.characters.forEach(function (character) {
character.destroy();
});
this.characters.length = 0;
};
TextMesh.prototype.createText = function (parent=undefined) {
// 引数parent で3DテキストのEntityを入れる親Entityを設定可能
if (!parent) {
this.destroyCharacters();
}
const font = this.fontData;
const scalar = this.characterSize / font.unitsPerEm;
var w = this.calculateWidth();
var cursor = 0;
switch (this.alignment) {
case 0:
break;
case 1:
cursor = -w * 0.5;
break;
case 2:
cursor = -w;
break;
}
var material = this.material ? this.material.resource : new pc.StandardMaterial();
for (var i = 0; i < this.text.length; i++) {
var character = this.text.charAt(i);
var glyph = font.charToGlyph(character);
var glyphData = this.parseCommands(glyph.path.commands);
if (glyphData.vertices.length > 0) {
var graphicsDevice = this.app.graphicsDevice;
// Create a new mesh from the glyph data
var mesh = new pc.Mesh(graphicsDevice);
mesh.setPositions(glyphData.vertices);
mesh.setNormals(glyphData.normals);
mesh.setIndices(glyphData.indices);
mesh.update(pc.PRIMITIVE_TRIANGLES);
var meshInstance = new pc.MeshInstance(mesh, material);
// Add a child entity for this character
var entity = new pc.Entity(character);
entity.addComponent('render', {
meshInstances: [ meshInstance ],
renderStyle: this.renderStyle
});
entity.setLocalPosition(cursor, 0, 0);
if(!parent) {
// scriptを登録したEntityの子要素として追加
this.entity.addChild(entity);
// 3Dテキストを配列に追加
this.characters.push(entity);
} else {
// parent のEntityに子要素として追加
parent.addChild(entity);
// 位置と向きを設定
entity.setPosition(this.entity.children[i].getPosition());
entity.setEulerAngles(this.entity.getEulerAngles());
// 物理演算を処理させるためにrigidbodyとcollisionを追加
entity.addComponent("rigidbody", { type: "dynamic" });
entity.addComponent("collision", { type: "box", halfExtents: new pc.Vec3(0.2, 0.2, 0.2), linearOffset: new pc.Vec3(0.1, 0.1, 0.1) });
}
}
if (i < this.text.length - 1) {
var nextGlyph = font.charToGlyph(this.text.charAt(i + 1));
cursor += font.getKerningValue(glyph, nextGlyph) * this.kerning * scalar;
}
cursor += glyph.advanceWidth * scalar + this.characterSpacing;
}
};
ここでは引数を受け取ったエンティティに複製した3Dテキストを追加するように処理を追記しました。
また、複製だけでは背景を転げ落ちないので、rigidbodyとcollisionをコード内で追加しています。
引数が無い場合には、今まで通りの挙動で複製はしないようにしています。
inputCreate.js は、Button Createエンティティに追加します。
スクリプト属性のmeshTextには、Mesh Textエンティティを設定します。
Launchで確認し、テキストを入力後にCREATEをクリックして3Dテキストが背景を転がり落ちたらOKです。
これで一通りのコンテンツは出来ました
本チュートリアルはここで一区切りとなります。
これより先は本チュートリアルの本質とは異なるが、スマホ対応やUIの細かい調整などを突き詰めた内容となっております。
ご興味があれば、是非このままチュートリアルを続行ください。
Chapter06 - UI調整とスマホ対応
UIの入力カーソルを作成する
現状は入力が出来る状態かぱっと見ではわからない状態になっています。
画像のような入力時にカーソルを追加し、実際のテキスト入力フォームの様に点滅を再現してみます。
ヒエラルキーからInput Textエンティティ配下の末端にImage Elementを追加します。
設定を以下のように変更していきます。
- Enabled: false
- Position:
- x: 9, y: 0, z: 0
- Element
- Preset : Right Anchor & Pivot
- Width : 3
- Height : 40
- Color : 黒に変更
PresetをRightに設定することで、親エンティティのTextエンティティの右に常に付いてくるように設定しています。
次は点滅させるためのスクリプトを追加します。
ASSETSのScriptsフォルダに新しく opacityInterval.js を作成し、以下のコードに書き換えます。
var OpacityInterval = pc.createScript('opacityInterval');
OpacityInterval.attributes.add("time",{type:"number", default:1000});
OpacityInterval.prototype.initialize = function() {
var self = this;
window.setInterval(function(){self.opacityToggle(self)}, self.time);
};
OpacityInterval.prototype.opacityToggle = function(self) {
self.entity.element.mask = !self.entity.element.mask;
};
setIntervalを使って一定間隔毎にElementのMaskのBooleansを切り替えて点滅を実現します。
opacityInterval.js を先ほど作成したImageエンティティに追加します。
スクリプト属性のtimeは、500に変更します。
これではクリックしてもカーソルがEnabledがfalseのため表示されないので、クリック時に表示されるように inputValue.js を以下のコードに書き換えます。
var InputValue = pc.createScript('inputValue');
// 3Dのテキストを作るEntityを設定
InputValue.attributes.add("meshText", {type:"entity"});
// Elementでテキストを表示しているEntity
InputValue.attributes.add("inputEntity", {type:"entity"});
// focusした時に表示するEntity
InputValue.attributes.add("focusEntity", {type:"entity"});
InputValue.prototype.initialize = function() {
var self = this;
// テキストを入力するためのinput要素を追加
self.inputElement = document.createElement("input");
self.inputElement.type = 'text';
self.inputElement.style.position = "fixed";
self.inputElement.style.bottom = 0;
self.inputElement.style.right = 0;
self.inputElement.style.zIndex = 1;
self.inputElement.style.width = "50px";
self.inputElement.style.height = "50px";
self.inputElement.style.fontSize = "2rem";
self.inputElement.style.opacity = 0;
self.inputElement.style.cursor = "pointer";
document.body.appendChild(self.inputElement);
// クリックされたらinput要素にfocusする
self.entity.element.on(pc.EVENT_MOUSEDOWN, self.inputFocus, this);
// inputにfocusしてキー入力を処理
self.inputElement.addEventListener("keydown",function(){
self.inputVal();
});
self.inputElement.addEventListener("keyup",function(){
self.inputVal();
});
self.inputElement.addEventListener("focus",function(){
self.focusEntity.enabled = true;
});
self.inputElement.addEventListener("blur",function(){
self.focusEntity.enabled = false;
});
};
InputValue.prototype.inputFocus = function() {
// input要素にfocusさせる
var self = this;
window.setTimeout(function() {
self.inputElement.focus();
}.bind(self), 90);
};
InputValue.prototype.inputVal = function() {
// input要素に入力されたテキストを textMesh.js の createText() で3D化
if(this.inputEntity){
this.inputEntity.element.text = this.inputElement.value;
}
this.meshText.script.textMesh.__attributes.text = this.inputElement.value;
this.meshText.script.textMesh.createText();
};
クリックするとinput要素にfocusイベントが発生します。これを利用して、focus, blurイベントを使用してenabledを切り替えます。
スクリプト属性に切り替えるエンティティを設定できるようにしています。先ほどのImageエンティティを設定しておきます。
これでfocusした時だけカーソルが表示され、点滅も確認が出来ます。
スマホ対応させるためにDOMでデザインを一新する
カーソルの点滅が出来たらスマホ対応も行いましょう。
主にclickイベントの箇所にtouchイベントを追加していく手順ですが、スマホ用のUIに変更もしていきます。
inputValue.js を以下のコードに書き換えます。
var InputValue = pc.createScript('inputValue');
// 3Dのテキストを作るEntityを設定
InputValue.attributes.add("meshText", {type:"entity"});
// Elementでテキストを表示しているEntity
InputValue.attributes.add("inputEntity", {type:"entity"});
// focusした時に表示するEntity
InputValue.attributes.add("focusEntity", {type:"entity"});
InputValue.prototype.initialize = function() {
var self = this;
// テキストを入力するためのinput要素を追加
self.inputElement = document.createElement("input");
self.inputElement.type = 'text';
self.inputElement.style.position = "fixed";
self.inputElement.style.bottom = 0;
self.inputElement.style.right = 0;
self.inputElement.style.zIndex = 1;
self.inputElement.style.width = "50px";
self.inputElement.style.height = "50px";
self.inputElement.style.fontSize = "2rem";
self.inputElement.style.opacity = 0;
self.inputElement.style.cursor = "pointer";
document.body.appendChild(self.inputElement);
if(!self.app.touch){
// クリックされたらinput要素にfocusする
self.entity.element.on(pc.EVENT_MOUSEDOWN, self.inputFocus, this);
} else {
// SP専用のDOMによるUI作成
self.entity.element.opacity = 0;
if(this.inputEntity){
self.inputEntity.element.opacity = 0;
}
self.divSpElement = document.createElement("div");
self.divSpElement.style.position = "fixed";
self.divSpElement.style.bottom = 0;
self.divSpElement.style.right = 0;
self.divSpElement.style.zIndex = 2;
self.divSpElement.style.width = "120px";
self.divSpElement.style.height = "20px";
self.divSpElement.style.padding = "20px 0";
self.divSpElement.style.borderTopLeftRadius = "25px";
self.divSpElement.style.backgroundColor = "#ffffff";
self.divSpElement.style.textAlign = "center";
self.divSpElement.style.cursor = "pointer";
self.spanSpElement = document.createElement("span");
self.spanSpElement.style.fontSize = "20px";
self.spanSpElement.style.fontWeight = "bold";
self.spanSpElement.style.lineHeight = 1;
self.spanSpElement.innerHTML = "TOUCH";
self.divSpElement.appendChild(self.spanSpElement);
document.body.appendChild(self.divSpElement);
// DOMのタッチイベント作成
self.divSpElement.addEventListener("touchstart",function(e){
e.preventDefault();
self.inputElement.focus();
});
}
// inputにfocusしてキー入力を処理
self.inputElement.addEventListener("keydown",function(){
self.inputVal();
});
self.inputElement.addEventListener("keyup",function(){
self.inputVal();
});
if(!self.app.touch){
self.inputElement.addEventListener("focus",function(){
self.focusEntity.enabled = true;
});
self.inputElement.addEventListener("blur",function(){
self.focusEntity.enabled = false;
});
}
};
InputValue.prototype.inputFocus = function() {
// input要素にfocusさせる
var self = this;
window.setTimeout(function() {
self.inputElement.focus();
}.bind(self), 90);
};
InputValue.prototype.inputVal = function() {
// input要素に入力されたテキストを textMesh.js の createText() で3D化
if(this.inputEntity){
this.inputEntity.element.text = this.inputElement.value;
}
this.meshText.script.textMesh.__attributes.text = this.inputElement.value;
this.meshText.script.textMesh.createText();
};
inputCreate.js も以下のコードに書き換えます。
var InputCreate = pc.createScript('inputCreate');
// 3Dのテキストを作るEntityを設定
InputCreate.attributes.add("meshText", {type: "entity"});
InputCreate.prototype.initialize = function() {
var self = this;
// 作成した3Dのテキストを入れる親Entityを作成
self.rigidGroup = new pc.Entity();
self.rigidGroup.name = "RigidGroup";
self.app.root.addChild(self.rigidGroup);
if(!this.app.touch){
self.entity.element.on(pc.EVENT_MOUSEDOWN, self.buttonClick, this);
} else {
self.entity.element.on(pc.EVENT_TOUCHSTART, self.buttonClick, this);
// SP専用のDOMによるUI作成
self.entity.element.opacity = 0;
self.entity.children[0].element.opacity = 0;
self.entity.element.on(pc.EVENT_TOUCHSTART, self.inputFocus, this);
self.divSpElement = document.createElement("div");
self.divSpElement.style.position = "fixed";
self.divSpElement.style.bottom = 0;
self.divSpElement.style.left = 0;
self.divSpElement.style.zIndex = 2;
self.divSpElement.style.width = "120px";
self.divSpElement.style.height = "20px";
self.divSpElement.style.padding = "20px 0";
self.divSpElement.style.borderTopRightRadius = "25px";
self.divSpElement.style.backgroundColor = "#111111";
self.divSpElement.style.textAlign = "center";
self.divSpElement.style.cursor = "pointer";
self.spanSpElement = document.createElement("span");
self.spanSpElement.style.color = "#ffffff";
self.spanSpElement.style.fontSize = "20px";
self.spanSpElement.style.fontWeight = "bold";
self.spanSpElement.style.lineHeight = 1;
self.spanSpElement.innerHTML = "CREATE";
self.divSpElement.appendChild(self.spanSpElement);
document.body.appendChild(self.divSpElement);
self.divSpElement.addEventListener("touchstart",function(e){
e.preventDefault();
self.buttonClick();
});
}
};
InputCreate.prototype.buttonClick = function() {
// input要素に入力されたテキストを textMesh.js の createText() で3D化
this.meshText.script.textMesh.createText(this.rigidGroup);
};
スマホ対応のためのデバイス判断は、 if(this.app.touch){} で行っています。
スマホ用のUIを表示するために、作成していたElementはopacityを0に変更し非表示となり、スマホ用のDOMを作成しています。
inputにfocusしてソフトウェアキーボードを表示させることでスマホに対応させています。
以上で、チュートリアルは完了となります。
3Dテキストの生成や、ElementによるUIなどを使いこなして、リッチコンテンツを作りましょう。
おつかれさまでした。
コメント
0件のコメント
サインインしてコメントを残してください。