XR Interaction Toolkitのコントローラー操作を攻略する

はじめに

この記事は STYLYアドベントカレンダー Advent Calendar 202115日目の記事です。

STYLYには大変お世話になっておりますが、今回はSTYLYに関係ないOculus Quest開発の話になります🙇‍♀️

テーマはUnity + XR Interaction Toolkitでのコントローラー操作について。今でこそ情報がよく纏まった記事もいくつか出ていますが、開発していた当初(主に1年〜半年前)はあまり情報がなく、特に「ものを掴む・移動する は簡単に実装できたけど、コントローラーに独自の機能を割り当てるにはどうすればいいんだ??」となっていたので一度まとめることにしました。

XR Interaction Toolkitのメリット・デメリット

※知っている方は読み飛ばしてください

2年前(2019)まではOculus Quest向けの開発といえばOculus公式の提供しているUnityアセット「Oculus Integration」の一強でした。

assetstore.unity.com

しかし2020年頃からUnity純正のツールキットとして「XR Interaction Toolkit」が登場します。私はこれをXRKaigi 2020の講演で知り、研究で使う実験環境を開発するために早速導入しました。

使ってみると、やはりメリットとしては

  • 「ものを掴む」「VR空間内を移動する」といったVRでの基本的な操作を非常に簡単に実装できる
  • 1つのプロジェクトでQuest、Viveなど複数のVRヘッドセットに対応できる

という点が挙げられると思います。

Oculus Integrationを使って初めてVR開発をしたときは、容量が重くビルド時間の長さに悩まされたり、本来なら利用できるはずのPrefabが利用できずやむなくスクリプトVR内を歩き回れるようにしたり、ただUIをクリックしたいだけなのにコントローラーのボタン取得に苦戦したりと様々な苦労をした覚えがあります。XR Interaction Toolkitでは最初から「テレポーテーション」「UIクリック」「ものを掴む」の役割を持つコンポーネントがあり、それを使いやすくしたサンプルプロジェクトもあり、だいぶ実装が楽になりました。これだけでも利用する価値は十分にあると思います。

一方のデメリットとしては

  • ベータ版であり、情報が少ない(=独自の操作を実装するのが少々大変

でしょうか。公式で出ている情報も少なく難解でまとまっていません。他にもUnityユーザーマニュアルの「UnityEngine.XR」という項目に情報が出ていたりしますが、初心者ではまずここに辿り着けません。。多少翻訳がおかしくても多くの情報が載った日本語ドキュメントがヒットするOculus Integrationとは大きく違います。

しかしこの情報が少ない中で、XR Kaigi 2020・2021で登壇されていたUnity Technologies Japan 高橋さんの講演は非常にわかりやすく、XR Interaction Toolkitを触ろうかなと思っている方にはぜひお勧めしたいです。

www.youtube.com

サンプルのデフォルト操作はどうなっている?

前置きが長くなりましたが、XR Interaction Toolkitのサンプルのコントローラー操作がどうなっているのかまず見ていきたいと思います。

開発環境は以下の通りです。

  • Oculus Quest 2
  • Unity 2020.3.24f1

今回はサンプルプロジェクトを使っていきます。このサンプルプロジェクトは優秀で、簡単なプロジェクトを作るだけであれば0からシーンを構築するよりもサンプルプロジェクトを編集して作った方が早いです。※サンプルプロジェクトをベースにしているため、0からシーンを構築する方は「新しく機能を実装する場合」の章まで読み飛ばしてください。

github.com

上記GitHubからプロジェクトをダウンロードし、Unityで開いたら、 Assets > Scenes > World Interaction Demo というシーンを開きます。

f:id:nubonubo:20211214134414p:plain

Inspectorから、XRRigDemo(=カメラにあたるもの)の中身を展開してみましょう。

f:id:nubonubo:20211214134132p:plain

※今回、LeftHandは使わないので非表示にしています。

RightHandにはRightBaseControllerという主に物体を掴んだりUI操作に使うコントローラーと、RightTeleportControllerという主にテレポーテーション移動に使うコントローラーが割り当てられています。

デフォルトではRightBaseControllerからRayが出ない状態だったので、XRDirect InteractorというコンポーネントをRemoveし、XR Ray Interactorコンポーネントを新しく追加します。(こちら も参考にさせていただきました)

f:id:nubonubo:20211214135355p:plain

この状態でビルドすると…(ビルドの仕方は上に貼った動画をご参照ください)

直線のRayが常に出ていて、人差し指のトリガーでUIをクリックできます。

f:id:nubonubo:20211214140422g:plain:w300

スティックボタンを上に倒す、もしくはAボタンでテレポートモード=曲線のRayに切り替わり、放すとRayの着地点に移動できます。

f:id:nubonubo:20211214141243g:plain:w300

スティックボタンを横に倒すことで視界が左右に45度ずつ回転します。スティックボタンを下に倒すと180度回転します。

f:id:nubonubo:20211214142524g:plain:w300

中指のトリガーを押すことでものを掴むことができます(さらに掴んだ状態で人差し指トリガーを押すことで発火するアクションも設定できるようです)。

f:id:nubonubo:20211214141825g:plain:w300

サンプルを使うだけでこんなに簡単に実装できてしまいました。

ただ、コントローラーのほとんどのボタンには「すでに機能が設定されている」わけです。

f:id:nubonubo:20211214192919j:plain:w350

独自機能を割り当てるには?

デフォルト機能を少し変えたい、無効にしたい場合

少し変える程度であればスクリプトを書く必要はなく、パラメーターの変更だけでできます。

例えば「スティックボタンを下に倒すと180度回転する」機能をオフにしたい場合。

RightBaseControllerやRightTeleportControllerにアタッチされているXRController(Action-based)コンポーネントの「Reference」で設定されている「XRI RightHand/~~~」をどれでもいいのでクリックします。

f:id:nubonubo:20211214151237p:plain

すると新しくXRI Default Input Actionsというウィンドウが開きます。どうやらHMD本体やコントローラーのあらゆるインプットについてはこちらで管理されているようです。

f:id:nubonubo:20211214152018p:plain

スティックボタンでの回転を管理しているのはこの中のTurn > Primary 2DAxis です。右側の「Interactions」にDirectionsがSouthとして設定されているものがあるので、これをNoneにすると、スティックボタンを下に倒す挙動をオフにできます。変更したら上の「Save Asset」ボタンを押すのを忘れないようにしましょう。

f:id:nubonubo:20211214153759p:plain

続いて、「スティックボタンを横に動かした時の回転を連続的にしたい」場合。デフォルトではVR酔い防止のために回転は45度ずつしか動かせなくなっていますが、回転角を変えたり、連続的に動くように変更することもできます。この回転はXRRigDemoのSnap Turn Providerで定義されています(ここで、先程のXRI RightHandが呼び出されています)。

f:id:nubonubo:20211214170151p:plain

Turn Amountを2、Debounce Time(=この時間の間は何回入力しても1回とカウントされる)を0.05に設定してみます。

f:id:nubonubo:20211214170816g:plain

スムーズに動かすことができるようになりました。なお、これより小さい値を設定してしまうとVR酔いが発生してしまうと思います。

新しく機能を実装する場合

この情報になかなか辿り着けなかったのですが、CommonUsage.primaryButtonのような形でボタンの状態を取得することができるようです。

docs.unity3d.com

コントローラーとの対応表はこちら。

docs.unity3d.com

今回上のマニュアルの、「primaryButtonの例」に記載されているコードで試してみました。どうやらこれは、primaryButton(=QuestではAボタン)を押すと、配置されたCubeが回転するというコードのようです。

適当なGameObjectにPrimaryButtonWatcherクラスを割り当て、ワールド上に配置したCubeにPrimaryReactorクラスを割り当て、紐付けます。

f:id:nubonubo:20211214185530p:plain

f:id:nubonubo:20211214185539p:plain

では、試してみると…

f:id:nubonubo:20211214190018g:plain

おわかりいただけたでしょうか。。確かにCubeは回転しているのですが、Aボタンを離した瞬間、Aボタンに元々割り当てられている「テレポートモードへの切り替え」が発動してしまっています。

つまり元々割り当てられている機能を上書きしてくれるわけではなくて、元々割り当てられている機能は無効化しておく必要があるんですね。

なので一つ前の章で書いた通り、XRI Default Input Actionsから該当箇所を無効化しておきましょう。

f:id:nubonubo:20211214190451p:plain

f:id:nubonubo:20211214190944g:plain

回転だけできるようになりました!

こんな複雑なコード書かなくてももっと簡単に書けるような気がしますが、要検証ですね…(試しにちらっとやったらうまくいかなかったので、今回は諦めます)

まとめ

XR Interaction Toolkit、まだまだ情報が少なくはありますが、デフォルトの操作を使いこなして+スクリプトで独自の操作を実装できれば、操作周りで大抵のやりたいことは実現できるんじゃないでしょうか。デフォルトの機能が本当に充実しているので、使いやすいと思います。

f:id:nubonubo:20211214192144g:plain
スナップ機能まである

もっとユーザーが増えて情報が増えることを願っています!

PLATEAU HACK AWARDで得られた地形データの知見

前置き

先日(だいぶ前になってしまったけど)ブログに書いた通り、PLATEAU HACK AWARDに参加してきました。

makumakuworks.hatenablog.com

そこで得られた知見はチームワークだったり、Unityのプログラミングだったり色々あれど、突筆すべきは「地形データに関する知見」だったと思います。

おそらく大概のUnity開発者やWeb開発者はそうだと思うが、私はどんな地形データがどのように公開されているかなど、地形データに関しては完全に無知でした。建築土木系の方は一定詳しいかもしれないけれど。

ハッカソン中に調べたり専門家に尋ねたりしたことがいつか誰かの役に立つかもしれないので、ここに書き残しておきます。

街路樹のデータ

東京都建設局では、街路樹の主な樹種と本数を公開している。

www.kensetsu.metro.tokyo.lg.jp

一方で街路樹の座標データは台帳で管理されており一般公開されていないが、杉並区は独自に公開している。

catalog.data.metro.tokyo.lg.jp

(上のリンク先で、「URL」を直接ブラウザで入力するとダウンロードできる)

海外だとオープンデータになってる場合がある。

地域メッシュと平面直角座標

地域メッシュ:緯度経度の値に応じて地図を分割したもの。

club.informatix.co.jp

PLATEAUで使われているのは3次と4次。

以下のサイトから各マス目の緯度を割り出せる。

maps.multisoup.co.jp

PLATEAUのデータは平面直角座標(ただし現実のデータの1/100)を利用している。緯度経度に変換などの計算をするには以下の国土地理院のデータが正確。他のサイトは間違っていることがある。

vldb.gsi.go.jp

PLATEAUデータ

正直色んなところに散らばっていてわかりにくいと思う。

  • PLATEAU VIEWのオープンソースで公開されているコード github.com

  • 実際の3Dモデルダウンロード場所 www.geospatial.jp

  • Unityへのインポート方法:モデルが原点からずれているというのがポイントで、モデルを原点にずらす…か、カメラを移動する必要がある(カメラ移動させる方が楽かも、、)。 styly.cc

  • 3D都市モデルやCity GMLについてわかりやすく載っている www.mlit.go.jp

  • より詳しい使い方やデータ仕様 www.mlit.go.jp

ソフトやライブラリ

CityGMLのデータを閲覧するためのソフトには、QGISというものがある。

qgis.org

CesiumJS という、Webブラウザ上で操作する3D地図が描けるjsライブラリがある。

cesium.com

初めてハッカソンに参加して、賞をもらった話

ハッカソンに参加してきた

ちょうど先週、『PLATEAU Hack Challenge 2021』というハッカソンに参加し、審査員奨励賞をいただくことができた。

ascii.jp

私はこのハッカソンでリーダー兼エンジニアを務めた。結果は良かったものの、良かったこともモヤっとしたこともあったので、振り返りのメモを残しておきたい。

どんな人が多かったか?

学生から社会人まで、それも若手からベテランまで、かなり幅広い層の方が参加されていた。

第一回は資料を読む限りシミュレーションなどの方面に詳しい方が多かった印象だが、第二回である今回はVR界隈の人がやたらと多く、故にUnityを使うプロジェクトが大半だった。

何らかの開発スキルを持つ人が8割、他が2割くらい。アイデアソン経由で参加される方もいるので、アイデアはあるけど開発スキルはない、という人ももちろんいる。

チーム開発・リーダーの難しさ

元々私は3人のチームで応募をしていた。この3人でハッカソンこそ参加したことはなかったが、某ブランドデザインコンテストや企画コンペに応募したりしており、今回も事前に3人で企画の打ち合わせを週1ペースで行っていた。(このハッカソンは事前にアイデアを提出する珍しい形式をとっていた)

悩みに悩んだ末、私の発案したアイデアで応募することに決まり、ハッカソンへの参加提案とアイデアの発案者としてリーダーを務めることになった。(とはいえリーダー経験はあまりなく、稀にリーダーをやった時もあまりいい思い出がなかった。)

3人のメンバーは私がエンジニア・他2人はデザイナーで、流石に開発が間に合わないだろうということでチーム参加者をハッカソン当日に募ることにした。そして4人のメンバーが新たに加わることになった。年代もバラバラでかなりベテランの方とも一緒に開発ができて、本当に貴重な経験だった。4人に加わって頂いたおかげでこのプロジェクトを形にすることができ、賞までいただけたので、本当に感謝しかない。

…それは一旦置いておいて。

ただチーム開発というのは本当に難しい。皆が皆、「このプロジェクトがより良くなるように」と自分の役割に囚われず積極的に動いてもらえれば一番いいのだが、「リーダーが自分に振った仕事しかやらない」のがほとんどだと思う。オンラインなら尚更。 その点において、7人という人数は多すぎた。今回エンジニアは3~4人くらいで動いていてちょうどよかったのだが、それ以上増やすのは恐らくきつい。そしてデザイナーは1人いれば十分かもしれない。 ハッカソンのデザイン業務を2人で分けると、必ず「暇な時間」が発生してしまう。

暇な人がいるなら、仕事を振らないといけない。開発に追われていた私はあまりリーダーをやっている余裕がなく、暇な彼に「ディレクション頼みたい」と雑に頼んでしまった。彼がなんとなく得意そうなイメージがあったので無理に押し付けてしまったのだと思う。曖昧な指示でうまくいくわけもなく、結局少し開発に余裕ができたら私が全てディレクションをやっていた。

結果的に賞もとれて、チームメンバーの方々も概ね満足されていて、嬉しかった。が、そうじゃない人もいた。でも何が原因かわからない。思いつくことはいくつかあっても、真相はわからず、もやもやしたままだ。

正直、「誰か1人でも傷つけるくらいなら、自分はもうリーダーをやりたくない」とは、思ってしまった。(追記:気のせいだったかもしれない)

「物」を見るのか「人」を見るのか

リーダーをやってみて、リーダー(マネジメント・ディレクション)の役割は2つあると気づけた。1つ目は「物」を見る、つまり計画を立ててプロジェクトを正しく遂行する役割。そして2つ目は「人」を見る、つまりチームのモチベーションを保ち、その人の適性を見て正しく仕事を振る役割。

今回、私は前者に振りすぎていたと思う。ただ現時点で後者の適性があるとはあまり思えない。周りを見ていても、後者の才能がある人って結構いて、どんな人とも仲良くできる人、褒めるのが上手い人、まとめるのが上手い人、たくさん見てきた。

私はそういった人たちとは遠いから、と普段は裏方的なポジションでチームを支えることが多かったのだけど、就活していた時インターン(という名のグループワーク)では「裏方ポジションもいいけど、リーダー的ポジションもやっていけると思うよ」とメンターの方から御助言いただいたので、適性が全くないわけではないと思う。

なので最初から諦めず、どうやったら近づけるか、これから少しずつ考えていきたい。

ハッカソンに大事なのは技術力だけじゃない

今回のハッカソンは評価基準があらかじめ定められていた。

そう、これを見ればわかる通り、「技術力」は含まれていない。 このハッカソンで求められるのは、「PLATEAUでないと実現できない価値があるか」だと感じた。それGoogle Mapでできるよね?というアイデアではダメなのだ。私たちのチームではその部分をよく議論していた(ただの3Dモデルとしてではなく建物のデータをどう活かすかを考えていた)ので、良い結果に結びついた。

これは他のハッカソンでも同じだと思う。技術力は大事だが、技術力だけを重視するハッカソンなんてそうない。

なんだかんだ言ってハッカソンは楽しい!

前半で色々書いたものの、1週間たっても熱に浮かされている。知らないメンバーと開発するのも、他のこと全部忘れて徹夜で開発するのも性に合っていて、とにかくアドレナリンがすごかった。(逆にコツコツ開発するのが苦手すぎてどうすれば…)

ハッカソン参加したい欲」が昂って、次に参加する予定のハッカソンの目処も立ててしまった笑 参加する前まで「ハッカソンで足手まといになったらどうしよう…」とか全く勇気が出なかったのが嘘のよう。

こんな風に、短期間でモノを作る機会を作ることが自分のエンジンをかける方法なんだ、と気づけたのはとても大きい。

次もリーダー的役割をするかはわからないけれど、もしなったら今回の反省点は絶対に活かしていこう。

Swiper.jsで矢印をFont Awesomeに置き換える

実現したこと

前回に引き続き、スライダーを作るのに便利なjsライブラリ、Swiperの話です。

makumakuworks.hatenablog.com

矢印の部分を画像やFont Awesomeに置き換えたい、という記事はいくつか見つけたのですが、どれを試してももとの矢印が消えずうまくいかなったので、現時点でうまくいく方法を書いていきます。

前回の記事から流用したものになりますが、CodePenはこちら

つまずきポイント

まずはGoogleデベロッパーツールで矢印のCSSがどのように書かれているか見てみましょう。

するとどうやら、.swiper-button-prev、.swiper-button-nextの擬似要素(:after)にて、swiper-iconsという独自のフォントとして矢印アイコンが配置されているようです。

このため、.swiper-button-prev、.swiper-button-nextにCSS側で背景画像を設定したりimgタグを間に入れても、擬似要素側で描かれている元の矢印が消えることはありません。

どうやったか

無理矢理と言えば無理矢理ですが、.swiper-button-prev、.swiper-button-nextの擬似要素をdisplay: none;で消します。

.swiper-button-next:after, .swiper-button-prev:after {
   display: none;
}

その後、FontAwesomeを読み込みます。

<div class="swiper-button-prev fa-2x"><i class="fas fa-arrow-circle-left"></i></div>
<div class="swiper-button-next fa-2x"><i class="fas fa-arrow-circle-right"></i></div>

たったこれだけ!


参考

Swiper.jsで矢印だけをスライドの外側に配置する

実現したこと

Web制作においてスライダーを作るのに便利なjsライブラリ、Swiper。

swiperjs.com

Swiperを使い、以下のように「スライドは中央に寄せられているが、矢印は外側にある」状態を作りました。

ブログ上でうまく展開できなかったのですがCodepenのリンクはこちら

つまずきポイント

まずは公式ドキュメントのDemosのSlides per viewなどを参考にしながら、一回に3つスライドが表示され、かつ矢印ボタンが表示されるようにjs側で設定します。

const swiper = new Swiper('.swiper-container', {
    // Optional parameters
    slidesPerView: 3,
    spaceBetween: 10,

    // Navigation arrows
    navigation: {
      nextEl: '.swiper-button-next',
      prevEl: '.swiper-button-prev',
    },
  });

SwiperのHTML・CSS構造がどうなっているかを見ておきましょう。

  <div class="swiper-container">
    <!-- Additional required wrapper -->
    <div class="swiper-wrapper">
      <!-- Slides -->
      <div class="swiper-slide"></div>
      <div class="swiper-slide"></div>
      <div class="swiper-slide"></div>
      <div class="swiper-slide"></div>
      <div class="swiper-slide"></div>
    </div>
  </div>
  <div class="swiper-navWrap">
  <!-- If we need navigation buttons -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

まずはswiper-container。

(画像上では完成しています)

.swiper-container {
    width: 770px;
    height: 100px;
}

widthには各スライドの幅×3+スライドの間の幅 で770pxを指定しました。 .swiper-container にはライブラリ側でoverflow: hiddenが適用されているため、widthより外側=margin部分は表示されません。

続いてswiper-wrapper。

(画像上では完成しています)

wrapperにはcontainerで隠されているスライドも含めた、全スライドが入っています。 ここではライブラリ側で指定されているもの以外、特にCSSを指定していません。

これでスライド部分は大体完成、あとはswiper-button-prevとswiper-button-nextの位置を変えればいいのでは、という話なのですが、 実はswiper-containerにoverflow: hiddenが適用されているため、矢印ボタンを外にはみ出させようとすると要素が表示されなくなってしまうのです。

overflow: visibleなどを別途設定しても、挙動が変わってしまいます。

どうやったか

そこで対処策としては、swiper-button-prevとswiper-button-nextの2つを別のdivで覆ってしまうこと。

<div class="swiper-navWrap">
    <!-- If we need navigation buttons -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
.swiper-navWrap{
    display: flex;
    position: absolute;
    width: 100%;
    height: 50px;
    top: 0;
    left: 0;
}

これで矢印が隠れることなく表示されます。あとは位置を微調整するだけ。

【Oculus Quest2開発】HMDの視界に枠をつける

実現したこと

HMDで全天球映像を見る際に、あえて視界を狭くしたかったので、視界に黒い枠を固定でつけることを考えました。

f:id:nubonubo:20210621022359g:plain

周囲が黒い枠で囲まれているのがお分かり頂けるでしょうか。 PCで見るとそうでもないのですが、VRで見るとさらに周囲の枠が太くなり視界が狭まっています。

どうやったか

やっていることは簡単で、VRカメラの子オブジェクトとして黒枠を配置すればいいだけです。 詳しく見ていきましょう。

まずillustratorなどで真ん中を抜いた黒枠を作り、Unityに読み込んでおきます。(結構黒枠を太めに作らないと枠が見えません。。)

f:id:nubonubo:20210621023222p:plain

VRカメラ(ここではXR Interaction Toolkitを利用しているため、XRRigDemo > CameraOffset > Main Camera)を右クリックし子オブジェクトとしてCanvasを追加します。

f:id:nubonubo:20210621023407p:plain

Canvasのインスペクターから、Render Modeを「Screen Space - Camera」、Render Cameraに先程のMain Cameraをドラッグ&ドロップして設定します。

f:id:nubonubo:20210621023518p:plain

下部の「Add Component」からImageコンポーネントを割り当てます。Source Imageに黒枠の画像をドラッグ&ドロップでセットします。セットできない場合、黒枠の画像のインスペクターからTexture Typeを「Sprite(2D & UI)」に変更してください。

f:id:nubonubo:20210621023703p:plain

するとエディタ上ではこうなります。

f:id:nubonubo:20210621023900p:plain

これで設定は完了です。

f:id:nubonubo:20210621022359g:plain

Render Modeの「Screen Space - Camera」は他のオブジェクトとの前後関係が考慮されるため、今回はUIを黒枠より手前に配置しており、UIは黒枠の中からはみ出ても見えるようになっています(これがいいか悪いかは別として)。

(7/16追記)実際にどこまで見えるのか検証

実はHMDで見ると枠は見えづらいです。

HMDでの見え方を検証するために、3Dモデリングによく使われるグリッドと、枠を重ね合わせた画像を作り検証してみました。

上で使用しているグリッドより少し見える範囲を広くしたものを利用しています(単純に間違えました)

f:id:nubonubo:20210717070152p:plain

f:id:nubonubo:20210717064825p:plain

これをHMDで見ると…

f:id:nubonubo:20210717065336j:plain

HMDの視野は、正方形に近いです。一方で枠は長方形のため、横はあまり視野が狭まっていません。 その代わり縦には狭くなっているので、ちゃんと「枠がある」ことは認識できます。

ちなみに、グリッドは歪まず綺麗に見えていました。


参考

PerlでPukiWikiからMarkdownに半自動変換する

PukiWikiMarkdown移行の経緯

少し前になりますがバイト先でPukiWikiに溜めてきたノウハウをプリザンターというプラットフォームに移行することになりました。

しかしPukiWikiPukiWiki記法で書かれているのに対し、プリザンターはMarkdown記法。手動で直すにはあまりに膨大な量で、なんとか半自動で移行できないかと調べてたらこちらのワンライナーというものを発見。

qiita.com

Macのターミナルで実行できました。

基本構文は以下(wikiの文章はあらかじめtxtデータとして保存しておきます)

cat pukiwiki.txt | sed -e 's/ \[#[0-9a-z]\+\]$//g' -e 's/~$//g' | less > markdown.txt
  • cat:ファイルまたは標準入力の内容をそのまま標準出力に出力するコマンド。
  • |:通称パイプ。複数のコマンドをつなぎ、標準出力を次のコマンドに渡す。
  • sed:文字列を全置換したり、行単位で抽出したり、削除したり、いろいろなテキスト処理のできるコマンド。
  • -e:perlワンライナーとして利用する。その後ろに実行したいperlスクリプトを''で囲って指定。
  • less:入力の全部をターミナル上でスクロールしながら見る。catだとターミナル上を一瞬で流れてしまうので、最後の1画面分以外は見ることができない。
  • >:表示されている内容をファイルに書き込む。

ただし上のコマンドだと一部の拡張正規表現が使えません。 なので、以下のように変えます。

cat test.txt | perl -pe 's/ \[#[0-9a-z]+\]$//g; s/~$//g;' | less > markdown.txt
  • sedは文字列に特化したコマンドだが、perlならさらに複雑な処理も覚えればできる。
  • -p :perlsedっぽくするオプション。

コマンドの''内の読み方

上のコマンドで特に''で囲まれている部分は何やら訳のわからない文字列の集合に見えますが、以下の法則を元に作られています。

変換の基本構文

s/hogehoge/fugafuga/g;

という文があれば、hogehogeがfugafugaに変換されます。

正規表現

これに関してはいろんな方がまとめてくださっているので、そちらを参考にしてください!

userweb.mnet.ne.jp

メタ文字

上の正規表現で使われている記号(=メタ文字)は普通に書いただけでは検索対象として認識されません。

. ^ $ [ ] * + ? | ( )

を検索対象としたい場合は、以下のように表記します。

\. \^ \$ \[ \] \* \+ \? \| \( \)

\ 自体は、\と書きます。

なお、Windowsの場合は上の\は全て¥です。

実例から学ぶ

以下、実際に使った正規表現のコマンドをまとめておきます。

末尾のハッシュを削る

-e 's/ \[#[0-9a-z]+\]$//g'
  • []で囲むと、[]内のいずれかという意味になる。[0-9][a-z]を連続して書くとこうなる。
  • +は直前のパターンの1回以上繰り返し
  • 最後の$は文末であるという意味。

末尾の~を削る

-e 's/~$//g'

#で始まる文一括削除

-e 's/^#.*//g'
  • 最初の^は文頭であるという意味。
  • .は任意の一文字、*は直前のパターンの0回以上繰り返しという意味なので、.*で任意の文字列という意味。

太字を変換(''→**)

-e 's/\'\'/\*\*/g'
  • この時、コマンド全体を囲む'を"に変更すること!

見出しを# に変更(*→# )

-e 's/^\*\*\*/### /g' -e 's/^\*\*/## /g' -e 's/^\*/# /g'
  • 半角スペースはそのまま半角空ければOK

箇条書きを*に変更(-→* )

-e 's/^---/ \* /g' -e 's/^--/ \* /g' -e 's/^-/\* /g'

1.の箇条書き(+→1. 、++→ *)

-e 's/^+++/ \* /g' -e 's/^++/ \* /g' -e 's/^+/1\. /g'

サイズ表記を削除(&size(20){文字}→文字)

-e 's/&size\(\d+\)\{(.*)\}/$1/g'
  • 文字の部分をそのまま維持したい場合、その部分を()で囲む。この場合は(.*)の部分。変換後の正規表現では該当箇所を$1と表記する。

リンク表記を変換

-e 's/\[\[(.*)>http:\/\/(.*)\]\]/\[$1\]\(http:\/\/$2\)/g'
-e 's/\[\[(.*)>https:\/\/(.*)\]\]/\[$1\]\(https:\/\/$2\)/g'
  • サイズ表記の項と同様。複数の文字を維持する時は$1、$2…と使える。

参考