レベルエンター山本大のブログ

面白いプログラミング教育を若い人たちに

BLOCKVROCKリファレンス目次はこちら

NFTとXMLの類似性

NFTについてひとこと言えるほどの力量はないけれど、やっぱり最近の以下の議論がとても面白かったので書きたくなってしまった。

kumagi.hatenablog.com

NFTとXMLはアルファベット3文字の略称という以外にも、「実現できることは比較的シンプルな技術でありながら、壮大な未来が語られる」あたりが似ているように思えています。

XML

2000年前半、当時IT業界新卒だった僕は、XML(Extensible Markup Language)は画期的な技術で、ソフトウェア開発技術者としては絶対に知っておくべき技術だと教えられました。

当時XMLについて勉強し、記憶しているのは以下のような文句です。

  • 企業や銀行のデータ連携(EDI)は全てXMLベースに置き換わる
  • 企業はXMLインターフェイスにして業務ノウハウ(ビジネスロジック)を売り買いする時代になる
  • 画面の定義もできる
  • 今後全ての設定ファイルがXMLに置き換わる
  • 医療カルテや定型外のデータが全てXMLになりどんなソフトでも交換可能になる
  • データベースに革命が起きXMLデータベースが覇権を握る
  • HTMLの親戚である。それどころか、HTMLもXMLの1つにすぎないのである

今振り返って聞くと、顧客が欲しかったこととエンジニアの実装の差がよくわかりますが、当時の僕にはまったくわからん技術でした。

さらに、これだけのことができる技術でありながら、勉強してみるとXMLというのは、実の所データ記述するルールぐらいのことでしかなく、さらに謎が深まるわけです。

XMLをわかりたくて仕方なかった僕は、本当によくわからんので勉強しようと思ってXMLマスターという試験を受け、それでもよくわからなくてXMLマスタープロフェッショナルの資格本に執筆者の一人として参加するという斜め上の展開にたどり着いたという思い出があります。

XMLマスター教科書プロフェッショナル | 株式会社クロノス 志賀 澄人, 株式会社クロノス 山本 大, XML技術者育成推進委員会 |本 | 通販 | Amazon

この本の評価は残念ながら当時からビミョーでした。

当時の、XML技術によって生み出される未来という喧伝には、いくつもの論理の飛躍がありました。

たとえば、電子カルテが全てXMLになってどんなソフトでも交換可能になるという未来像は

電子カルテXMLフォーマットが業界で統一されたらの話」という論理の飛躍があり

電子カルテを読み込むソフトがその統一フォーマットに対応したらの話」という飛躍があり

「多くの医師がその統一フォーマット準拠のソフトに切り替えたらの話」という飛躍がある

これだけの困難な変化が起こってはじめてXMLで、全ての電子カルテが交換可能になるのです。

というわけで、2000年当時にはSF(サイエンスフィクション)の域を超えない話でした。

NFT

NFT(Non-Fungible Token)も、紐解いていくと要するに取引が記録されて消えにくい証明書トークンであるというもの。

できることは割とシンプルでそれ以上のことはないんだけれど、NFTは大きな期待を持って次のように語られます。

  • NFTでアートやツイートが高額で売れる
  • NFTでデジタルの土地が売り買いできる
  • NFTでメタバースで商売して生きる人類がうまれる
  • NFTをベースとした組織が新しい組織の形態となる

現時点では、NFTをビジネスと紐づけて熱く語る人の中には、高額で取引される投機的な部分だけに興味があるという人が多いようです。

高額な取引が実際に行われているので、いつものバズワードよりも熱を帯びている気がします。

ただ、上記のNFTが生み出す未来の中には、多大な論理の飛躍やいくつかの技術的・社会的なブレイクスルーがあって初めて成り立つことが、すぐにでも起こる未来のように語られている部分もあるように思います。

その期待値によってNFTの現在の価値が釣り上がるのがバブル的です。

論理の飛躍とともに語られる姿は、XMLのそれと近いと感じました。

NFTとメタバースの文脈での、論理の飛躍を含んだシナリオは例えば以下のような感じでしょうか。

「多くの人がメタバースに参加する必然性がある状態になったらの話」

「多くのメタバースを名乗る仮想空間プラットフォームが業界標準を作ったらの話」

「各社のプラットフォームが喧嘩をせずに標準に参画したらの話」

「次にアイテムや土地などの希少性が皆が認める状態になったらの話」

というようなSFが現時点では残っています。

個別の仮想空間プラットフォームが、標準化ないしは統合されて一つの大きなメタバースになるところが特に不透明で、上述した「標準化」というようなこれまで人類が何度も挑戦して失敗したプロセスが発生するシナリオをとるぐらいなら、巨大な1つの仮想空間プラットフォームが他を飲み込んで大きくなるシナリオの方がしっくりくるようにも思います。

ただ、そんな単一企業が独占する仮想空間プラットフォームだとしたら、NFTで土地やアイテムのやりとりを管理するよりも、単なるDBで管理した方がよっぽど理にかなっていてNFTの出番は無くなってしまいます。

ややこしいのは、いままででもソーシャルゲームの世界でNFTを使わなくても十分に成り立っていた土地の売買やアイテムの売買が、NFTによって実現されている出来事のように語られていることです。

NFTとXMLとSF

NFTにおけるSFも、XMLのその後に近くなるのではないでしょうか。

つまりNFTが作り出す未来として語られているSFのいくつかは別の方法で実現され、NFTは本質的に合っている用途(転売防止やデジタルデータの持ち主証明)などで進化する未来です。

XMLはデータフォーマットとしての地位は、JSONにとってかわられましたが言語やOSを問わないデータフォーマットのニーズはまやかしではなかった。

XMLによるEDIはできたところもあったができなかったところもあった。やっぱり標準化が安易にできるとは思わない方がいいですね。

SOAPという通信は今では見かけなくなったけれど、RESTやGraphQLにその遺伝子を残し、概ねXMLでやりたかったことは実現したように思います。

ビジネスロジックは売り買いできませんでしたね。

XMLを勉強していた当時、これは絶対無理だろうと思っていたUDDIというアイデアがありました。

SOAP通信のインターフェイス定義を全世界統一のUDDIというディレクトリで管理してシステム連携先を選ぶようなプロトコルで、データ連携の接続先を自動で切り替えることまで視野にいれていました。

流石にそれは無理があったのですが、システム開発を楽にする方法としてRESTful APIで様々なサービスを接続先に選ぶことはできるようになったり、

npmなどのパッケージマネージャが参照するグローバルなパッケージのリポジトリがあったり、当時思い描いたものとは異なるけれども便利な世界が実現されています。

NFTも課題が残る部分(よく言われるガス代とか環境負担とか)をそのままに発展するとは考えにくく、代替技術が出てきてからが本番というように感じています。

標準化みたいな時間のかかる論理の飛躍が含まれている未来像は、20年スパンぐらいでコモディティな感じになってから現実的になってくるような感じではないでしょうか。

そういう感じで、実現することとしないことがありつつ、一部別の形で実現し進むのでしょうね。

テクノロジーや人類の発展にはSFの力も必要だと僕は思います。

人間には想像できることしか実現できないので、XMLが出た時に夢のような未来を描くことがなければ、実現に向けて現実感をもって投資したり熱意を持って開発したりできなかったと思います。

過去の多くのSFでも予想ができなかった未来に我々は生きています。

ドラえもんの道具や、BTTFの2015年など答え合わせがされていっていて、こらから先の未来を作るためには、もっとSFが必要なんだろうなって思います。

Typescriptで国際化対応する方法を試行錯誤したので、viteつかってハンズオンできるように残す

f:id:iad_otomamay:20220207221801g:plain
国際化対応の完成図

Typescriptを使った開発で、国際化対応(多国語へのコンテンツ変換)に対応し、できるだけ開発体験をよくするための方法を考えてみました。

この記事では、typescriptの環境を作るためにviteでバニラtypescript(フレームワークを使わないシンプルなtypescript)のプロジェクトを作ってみることもやってみます。

僕は、自分のサービスで国際化対応するのは初めてではなく、なんどか試みたことがあるのですが開発体験がものすごく下がることが辛くて、一度挫折しました。

その課題を解決する方法を模索していて、ある程度納得のいく環境ができたので記事にします。

国際化対応のつらさ(課題)

当初はi18nextというパッケージでen.jsonとかja.jsonなどのリソースファイルを作ってやってましたが、以下のような辛さがありました。

  • ソース中を文字列などでgrepした時にリソースファイルに飛んでしまう
  • リソースファイルと本体のソースコードを行ったり来たりする際に迷子になる
  • 別言語での書き方を参考にしたい時に、リソースファイルが別れているのでいちいち探すのがつらい

どんな形が自分には理想的か?

開発の規模や国際化対応の規模によって、全然話が変わってくるのですが、僕の場合とりあえず日・ひらがな・英ぐらいで対応できたらいいだけなので小規模前提です。

  • 国際化の必要な文字リソースはプロジェクトのあらゆる場所に頻繁に出てくるので、1文字でも記述量を減らしたいし補完で書きたい。
  • リソースファイルの定義では、各言語が別ファイルにあるのではなく同一語句はまとめて定義したい。
  • リソースファイルは、機能群毎に分割したい。
  • IDEの機能でリソースファイルにすぐに飛びたい。
  • Typescriptの補完が効くのがベストだけれど、そのために書くべきことが増えるのはだめ。

ということで試行錯誤した結果、以下のような書き方に落ち着きました。

  • 利用する側
const title = translate.to("こんにちは世界", "title");

app.innerHTML = `<h1>${title}</h1>`;

#to()の第1引数に、日本語でのリソースを記述することによって、ソースの可読性やgrepした時の課題を解決します。

#to()の第2引数は、リソースのキーを記述します。これは定義されたリソースファイルのプロパティ名で補完されます。(後述しますがtypescript のkeyofがミソです)

translateオブジェクトの参照を辿れば、リソースファイルへはすぐに辿り着けるようにしました。

書き方を変えて以下のようにHTMLやVueテンプレートなどに埋め込むことも想定します。

translateを短くtに置き換えておくとテンプレートの中でも邪魔にならないですね。

  • テンプレートに文字列を埋め込む
const t = translate;

app.innerHTML = `<h1>${t.to("こんにちは世界", "title")}</h1>`;

そしてリソース定義側は以下のように同じリソース名に対する翻訳は、並べて記述します。

  • リソース定義
import { Translation } from "./Translation";

class TopResource extends Translation {
  title = {
    en: "Hello world!",
    cn: "你好世界!",
  };
 foo = {
   en: "Bar",
   cn: "婆",
 }
}
export const topResource = new TopResource();

translateオブジェクトは機能単位で分割可能で、Translationクラスを継承すればOKです。

リソースが増えても、toの第2引数はリソースのプロパティ(titleやfoo)で補完でき、型チェックも効いてくれます!

ではそんな環境を作っていきましょう

ハンズオン開始!viteプロジェクトを作成

vite は最近出てきたフロントエンドのツールで、ややこしかったビルド環境の構築を瞬時にやってくれるすごいやつです。 React、VueといったフレームワークだけではなくバニラなTypescriptの環境も作ってくれます(ちゃんとimportなどを解決してくれます)

以下のコマンドを実行します。

npm init vite@latest

そうすると対話式で、プロジェクトのScaffoldをつくてくれます。 まずはプロジェクト名、今回は「localization」と名づけました。

Project name: › localization

次に利用するフレームワークですが、ここは「vanilla」つまりフレームワークなしを選択しました。選択肢が広くていいですね。

? Select a framework: › - Use arrow-keys. Return to submit.
❯   vanilla
  vue
  react
  preact
  lit
  svelte

次は、JSかTSか。「vanilla-ts」を選びました。

? Select a variant: › - Use arrow-keys. Return to submit.
vanilla
❯   vanilla-ts

「Done. 」

Scaffolding project in /Users/dai/projects/2022/localization...

Done. Now run:

出来上がったプロジェクトのtreeを見てみると以下のような感じです。

.
├── favicon.svg
├── index.html
├── package.json
├── src
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
└── tsconfig.json

node_modulesがないのでnpm installを実行しておきます。

これだけで、VanillaなTypescript環境が出来上がりました。

package.jsonにあるdev のscriptをnpm runで実行しましょう。

npm run dev

ブラウザで「http://localhost:3000」にアクセスします。

f:id:iad_otomamay:20220207214947p:plain
Vite初期画面

VanillaなTypescriptをまともに作ろうと思うと、これまではどうしてもWebPackやらが必要になっていたのでありがたいです。

以降では、国際化対応のコードを記載していきます。

国際化対応のコードを記載

まずはi18next関連のライブラリをインストールします。

npm install i18next i18next-browser-languagedetector

次にリソースファイルです。

  • TopResource.ts(新規)
import { Translation } from "./Translation";

class TopResource extends Translation {
  title = {
    en: "Hello world!",
    cn: "你好世界!",
  };
 foo = {
   en: "Bar",
   cn: "婆",
 }
}
export const topResource = new TopResource();

次のクラスが翻訳を実施するクラスです。

  • Translation.ts(新規)
import i18next from "i18next";
import i18nextBrowserLanguageDetector from "i18next-browser-languagedetector";

/**
 * 対応言語を設定
 */
const Languages = ["en", "ja", "cn"] as const;
type LanguageTypes = typeof Languages[number];

/**
 * リソースファイルの親クラス
 */
export class Translation {
  constructor() {
    i18next.use(i18nextBrowserLanguageDetector).init();
  }

  // 対応する言語かを判定
  isAvailable = (name: string): name is LanguageTypes => {
    return Languages.some((value) => value === name);
  };

  /**
   * 現在選択されている言語を返す、リストになければenを返す
   */
  get language(): LanguageTypes {
    return this.isAvailable(i18next.language) ? i18next.language : "en";
  }

  set language(lang: LanguageTypes) {
    i18next.changeLanguage(lang).catch();
  }

  /**
   * リソースの翻訳を実施する
   * @param defaultJp 日本語のリソース文字列
   * @param key リソースを探すキーワード(リソースファイルのプロパティ名に一致)
   * @returns 翻訳された文字列
   */
  to(defaultJp: string, key: keyof this) {
    if (this.language === "ja") return defaultJp;
    const keyObject = this[key] as any;
    return keyObject[this.language] as string;
  }
}

ポイントは、toのメソッドです。

to(defaultJp: string, key: keyof this) 

引数keyの型がkeyof thisとなっており、継承サブクラスであるResourceクラスのプロパティ名をstring literal union 型にしています。

初めて使ったキーワードですが、なかなか感動!

これでkeyの指定をするために""などの文字列のクオーテーションを書くだけでリソースファイルからキーの一覧が出てきます。

そのほかのテクニックとしては、対応言語をstring literal union typeで指定しつつ、その内容も値として使いたい(string literal unionに含まれるかをチェックしたい)のでas constとtypeofをつかってstring literal unionを作っています。

画面を操作するmain.tsでは、各言語に変換できるボタンを実装しておきます。 宣言的にはかけないので頑張ります。

  • main.ts (修正)
import "./style.css";
import { topResource } from "./TopResource";

const app = document.querySelector<HTMLDivElement>("#app")!;
const t = topResource;

app.innerHTML = `
  <h1 id="title">${t.to("こんにちは世界", "title")}</h1>
  <button id="ja" class="lang"  >Japanese</button>
  <button id="en" class="lang"  >English</button>
  <button id="cn"  class="lang"  >Chinese</button>
`;

document.querySelectorAll(".lang").forEach((b) => {
  b.addEventListener("click", () => {
    t.language = b.id as "en" | "cn" | "ja";
    const title = t.to("こんにちは世界", "title");
    document.getElementById("title")!.innerText = title;
  });
});

できました。

f:id:iad_otomamay:20220207221801g:plain
国際化対応の完成図

人は炎上案件ではなく、教育で育てるべきなのだ。

ござ先輩が荒ぶってる!いいぞいいぞ!

gothedistance.hatenadiary.jp

 

大元は、ソフトウェア開発者1000万プレイヤーへの近道として、「炎上案件を経験したらいいぜ」みたいな話をしたソフトウェア会社の代表の話で、前後関係はわからないけどこの発言が拡散されて本気にとらえる人も多いし有害だわな。そんなの生存者バイアスでしかないのは21世紀の常識で、もうそう言う体育会系ブラック企業みたいな発言が許される世界はない。

 

しかしあったなー。こういう世界。生き残れたやつだけを次の地獄に叩き込んで育てていく世界。

 

僕もそうやって死の淵から生き残ったクチだが。だからこそ、そういう人の育成をしてはいけないと思ってる。DVの親に育てられた子は、親になったときどう育てていいかわからなくなるのと似てるんだろう。負の連鎖を断ち切るべし。

 

人は炎上案件ではなく、教育で育てるべきなのだ。

 

ちょうど先日、1人に230万円という中小企業では破格の教育費をかけて人を育成する企業の話を聞いたところだった。フォーブスの記事になってる。

 

forbesjapan.com

 

かっこいい。

ちょっと引用してみます。

 

研修だけで1人に、「約230万円」の投資──大阪の中小企業が見せる、本気の変貌

ビジネスパーソンとしての意識、仕事の進め方。

それらは多くの場合、新卒で入社した企業や若手時代に勤めた企業で基礎が作られ、良くも悪くも長年、体に染み付く。だからこそ社員の未来を本気で考える企業は、育成に投資をする。

 

 ほんまに「社員の未来を本気で考える会社は育成に投資する。」やね。

フリーランスや副業が全盛となり、大企業のメリットが昔ほどなくなってきた個の時代にあって、会社組織に何ができるのかと言えば、人を育成して利益を回し、育ち・巣立った人たちとも経済圏を広げて共栄していくことじゃないかなと。

 

「優秀なプレーヤーが増える中でも、選ばれ続ける企業にならないといけない。そのためには、ただの制作会社やただのシステム開発会社ではいけません。海外企業に負けない強みを身に付け、海外企業と日本企業のハブのような存在になる必要がある。今こそ、未来への投資が必要なのです」

 

選ばれ続けるために、コストをしっかりかけて人を育てていくこと、って言葉は当たり前のように感じるかもしれないですが、中小企業ってそんなに簡単じゃないから言葉が重い。

その仕組みのためには、安くしろとだけいう顧客とは付き合わないなど顧客もしっかりと選べなくては実現できない。

決して「下請け」とみなされない関係性の構築を重視する姿勢や、収益安定化の仕掛けが他にあるからこそ、こういうことができるんだな。それこそできる経営者というもんだな。

 

こういうかっこいい会社の方が、クローズアップされてほしいなー。

 

クォータニオン(Quaternion)をAframeで基礎から学ぶ

クォータニオン(Quaternion)は3次元空間の回転を表現するものです。 回転軸(3次元のベクトル)と回転角で成り立っているので、4要素があるので四元数(Quaternion)といいます。

A-frameでのrotationはオイラー角という方法で表現されています。これはxyzの3つの軸で回転する方法でわかりやすいのですが、複雑な回転になると使いづらいシーンがあります。

たとえば、すでに傾きのある物体に横回転を加えようと思います。

以下の動画では、左側の直方体をクリックするとオイラー角でY軸を中心に10度づつ回転するようにしました。 右側の直方体をクリックすると、クオータニオンで物体の上下を軸に10度づつ回転します。

f:id:iad_otomamay:20210516220346g:plain
クオータニオン回転とオイラー回転

オイラー角での回転は、Y軸の回転を徐々に増やすという書き方をしました。 今回のサンプルのようにすでに角度の付いた物体への回転は軸のずれたような回転になります。

this.el.addEventListener("click",()=>{
    this.el.object3D.rotation.y += 10;
});

クオーターニオンでの回転は複雑に見えますが、あるクオータニオンと別のクオータニオン内積を取ることで、ある回転した状態(姿勢)に別の回転をくわえた姿勢への移行が簡単に表現できます。

// Y軸のベクトル({x:0 y:0 z:0}から{x:0 y:1 z:0}のベクトルを作り回転させる)
this.yAxis = new THREE.Vector3( 0, 1, 0 );
// クオータ二オンの枠
this.quaternion = new THREE.Quaternion();
// 軸を中心に10度回線させるクオータ二オンを生成。(パイはラジアン角で180度)
this.quaternion.setFromAxisAngle( this.yAxis, Math.PI / 18   );

this.el.addEventListener("click",()=>{
    // クオーターニオンの内積によって現在の回転から、引数の回転を加えた姿勢に移行する。
    this.el.object3D.quaternion.multiply(this.quaternion );
});

上記と同じY軸10度のクオータニオンの生成は、以下のようにも書くことができます。 {x:0 y:1 z:0}のベクトル軸を中心に10度回線させるクオータ二オンをコンストラクタで一発で生成しています。 そしてnormalizeによって単位ベクトルに変換しておきます。単位ベクトルとは方向だけを表して長さを持たないベクトルです。

this.quaternion = new THREE.Quaternion(0, 1, 0, Math.PI / 18).normalize();

他にも、クォータニオン表現ではオイラー角表現で生じるような特異点ジンバルロック)が存在しなかったりと、とても便利な機能をもっていて3D空間で回転を扱うには必須の知識です。

コード全文

<html>
  <head>
    <meta charset="utf-8" />
    <title>quaternion sample</title>
    <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
  </head>
  <script type="text/javascript">
    AFRAME.registerComponent('quaterinon-rotation', {
      init: function () {
        // {x:0 y:0 z:0}から{x:0 y:1 z:0}のベクトル軸を中心に10度回線させるクオータ二オンを生成。(パイはラジアン角で180度)
        this.quaternion = new THREE.Quaternion(0, 1, 0, Math.PI / 18).normalize();

        this.el.addEventListener('click', () => {
          // クオーターニオンの内積によって現在の回転から、引数の回転を加えた姿勢に移行する。
          this.el.object3D.quaternion.multiply(this.quaternion);
        });
      },
    });
    AFRAME.registerComponent('euler-rotation', {
      init: function () {
        this.el.addEventListener('click', () => {
          this.el.object3D.rotation.y += 10;
        });
      },
    });
  </script>
  <body>
    <a-scene>
      <a-text value="click quaternion rotation" position="1 2 -4" color="black"></a-text>
      <a-box quaterinon-rotation position="2 1 -4" rotation="0 0 15" scale="1 1 2" init color="red"></a-box>

      <a-text value="click euler rotation" position="-2 2 -4" color="black"></a-text>
      <a-box euler-rotation position="-1 1 -4" rotation="0 0 15" scale="1 1 2" color="red"></a-box>

      <a-camera>
        <a-entity cursor="rayOrigin: mouse" />
      </a-camera>
    </a-scene>
  </body>
</html>

44歳にもなってプログラミングが楽しくてしょうがない

あと10日ほどで44歳になります。

で、いまはプログラミングが楽しくって、いやーどうしよう。ここ1−2年は人生で一番プログラムを書いたかもしれない。

あたらしいことをやるのも楽しい、VR/ARとかAIとか、ひと昔前ならSFだったことが実現できるのはとってもエキサイティング!

学ぶ中で、昔はわからなかった数学の難しい理論がちょっとづつわかってくるのも嬉しい。ゲームの腕前があがっていくのと同じで、着実に力がついてきてることがわかるのがいい。新たに学ぶことが、実利に繋がるのとても嬉しいしやる気がでる。「勉強」という強いられている感じがなく、探究な感じなのがとても楽しい。

 

今はプログラミングを使ってできることが急激に進化しているから、学べばありとあらゆることを仕事にできる。

 

プログラミングが若手のものだった時代

ちょっと前なら、プログラミングは若手がやる肉体労働で早々に卒業してマネジメントとかをやるのが大人、みたいな雰囲気がありました。自分も30代の頃よくそう言われたけどキャッチアップを続けていてよかった。ほんと。

何度、「そんなことはお前のやることじゃない」と言われたことか、、、

 

「プログラミングは若手のやること」論はわからんでもないです。基本細かい作業なので。

マネジメント力をつけて、たくさんの人を動かして大きな仕事をすることや組織や事業を大きくして社会へインパクトを与えることも面白いとは思います。営業という仕事の、価値を顧客に手渡しできてる感じも面白いですね。ビジネスのいろんな役割、どれもそれぞれ面白いと思える部分はあります。それはいいんだけど、直接手を下せない部分も増えるからもどかしい。大きな仕事の面白さと、手のひらにおさまる仕事の面白さはそれぞれにあると思う。

全部掌握できて、その上で一部を人に任せるのが理想で。いや理想は現実にできるとはいいませんが、目指すなかで落とし所を見つけたいな。

 

周りにもプログラミングする40代多い

10人ぐらい知り合いを思いおこしてみると、同年代のプログラマーも全然活躍しているように思います。CTOとか偉くなってる人も多いんですが、それでもバリバリコード書いてる人が多いです。

近年のソフトウェアは複雑で、一つの専門知識だけでは成り立たないですし、必然的に活躍する世代が上がっているんでしょうね。

35歳定年説とかもありましたね。きっとこれはちょっと前の大企業での話だったんでしょう。たしかに大企業だとマネジメントで大きな仕事を回すことが面白いと思います。でも、大企業含めてどんな企業でも変化を積極的に起こしていかなくてはいけない時代になってきてて、0−1みたいな仕事も多くなっているし時代が変わってる。

今まで、ITはゼネコンに喩えられていて、エンジニアは土方や建築士に例えられていたけど、今は料理人に例えるほうがしっくりくると思う。

腕のある料理人が、途中でマネージャーにならないといけない理屈はないし、自分で店を持つのが良いキャリアパスだなと思う。栄養士やレシピ屋さんになるのもいい。マネジメントは上手い人にまかせるか。

起業とプログラミングの相性

ということでプログラミングは、起業との相性もよい。ソフトウェアを作って売るって話じゃなくても、サービスに付加価値を作るためにも役立つし、業務のプロセスを効率化するためにも役立つ。アイデアを思いついたときに、実行するまでが早いし、そのたびに外部へ投資しなくて済む。起業+プログラミングで仕事をしてると、家族の介護みたいな状況もなんとかこなせてるし、いいことづくめ。

 

唯一の問題は、肩こりに悩まされることだけれど、腕があがるうちはやりたいです。

探究に終わりはないですね。よぼよぼのジジイになってもやっていたい。

 

 

 

a-frame raycasterを使って壁との衝突判定を実装する

a-frame raycasterを使って高低差のある地形に沿って移動する - レベルエンター山本大のブログの続き  

目的

a-frameではWASDキーおよび矢印キーでカメラの位置を移動させることができるものの、壁や物体への衝突判定は自分で作り込む必要があります。

以下の動画のように前進、右移動、左移動、後退のどの方向でも障害物に当たったら跳ね返されるようにすることがこの記事の目的です。

f:id:iad_otomamay:20210504014123g:plain

障害物に当たったら、その場にストップするという仕様も考えられますが、そうすると壁にめり込んだりというトラブルが発生しがちです。 めりこみを避けるためにぶつかったら、ちょっと跳ね返るという処理にしておきます。

準備

前回投稿した以下のプロジェクトを土台として、そこに付け足す形で実装します。 a-frame raycasterを使って高低差のある地形に沿って移動する - レベルエンター山本大のブログ

カメラに四方のレイキャスターを追加

前回、a-cameraにはstand-onというカスタムコンポーネントを設定し、下方向のレイキャスターを設定しました。

同じ仕組みで前方・後方・右側・左側のレイキャスターを設定します。

今回のカスタムコンポーネントは、wall-colliderという名前です。

a-cameraにwall-collider属性を複数設定しているため、__以下に識別子をつけた名前にしています。

この multiple instance の仕組みはComponent – A-Frameを参照してください。

raycasterは、directionによって前後左右に向くように設定します。

<a-camera
    wall-collider__f="raycaster:#raycaster-f"
    wall-collider__b="raycaster:#raycaster-b"
    wall-collider__r="raycaster:#raycaster-r"
    wall-collider__l="raycaster:#raycaster-l"
    stand-on="raycaster:#stand-on-ray"
>
    <a-entity id="stand-on-ray" raycaster="objects: .ground; direction:0 -1 0"></a-entity>

    <!-- 四方のレイキャスターを定義 -->
    <a-entity id="raycaster-f" raycaster="objects: .collidable; far: 1; direction:0 0 -1"></a-entity>
    <a-entity id="raycaster-b" raycaster="objects: .collidable; far: 1; direction:0 0 1"></a-entity>
    <a-entity id="raycaster-l" raycaster="objects: .collidable; far: 1; direction:1 0 0"></a-entity>
    <a-entity id="raycaster-r" raycaster="objects: .collidable; far: 1; direction:-1 0 0"></a-entity>
</a-camera>

カスタムコンポーネント

カメラに設定したwall-colliderコンポーネントを定義します。

a-frameのコンポーネントは、a-sceneより先に読み込まれている必要があります。

multiple:trueという属性を指定することで wall-collider__XXXのように、1要素に対して__区切りで複数のインスタンスを設定することができるようになります。

schemaやdependencyは前の記事を参照してください。

AFRAME.registerComponent('wall-collider', {
      schema: {
          raycaster: { type: 'selector' },
      },
      multiple: true, // 複数指定可能
      dependency: ['raycaster'],
}

初期化処理 initの実装

コンポーネントの初期化処理も、前回記事の地面に立つコンポーネントとよく似ています。

  1. カメラに設定したraycasterのいずれかが対象に交差したときにraycaster-intersectionイベントが発生します。

  2. そのタイミングで反応するべきraycasterであることを調べます。

  3. 交差先オブジェクトとの接面を保存しておきます。(次のtickのタイミングで使うため)

  4. 交差が外れたら保存したオブジェクトをクリアしておきます。

evt.detail.intersections[0].face.normalの記述部分だけが、前回と異なります。 交差オブジェクトとの接面(face)を取得して、nomalizeして保存しておくというコードです。

nomalizeすることで、ベクトルから距離の情報を取り除き、向きを表す単位ベクトルに変換することができます。 つまり、"0 1 0" や"0 0 -1"のようにxyzを0,1,-1だけで表すベクトルに変換されます。

init: function () {
    this.el.addEventListener('raycaster-intersection', (evt) => {
        console.log(evt.target);
        if (evt.target !== this.data.raycaster) return; // 対応するレイキャスターのみ反応
        this.face = evt.detail.intersections[0].face.normal;
    });

    this.el.addEventListener('raycaster-intersection-cleared', (evt) => {
        if (evt.target.id !== this.data.id) return;
        this.face = null;
    });
},

tickの実装

tickでは、faceが保存されていれば対象オブジェクトと衝突しているので、跳ね返すのですがthree.jsのメソッドを使えば、大変簡単な記述で実現できます。以下の処理だけ。

tick: function () {
    if (!this.face) return;
    this.el.object3D.position.add(this.face.multiplyScalar(0.5));
},

一つ一つ説明します。 this.faceが登録されていなければ、衝突していないので処理しないとします。

次にthis.faceが登録されている場合です。 カメラの要素に設定したコンポーネント内であるため、this.elはカメラのaframe要素となります。そのカメラ要素からobject3Dプロパティでthreejsのオブジェクトを取り出します。

three.jsのadd()メソッドは、ベクトル同士の和(足し算)を計算してくれるものです。

this.faceに保存された接面のベクトルは、衝突方向と反転したベクトルを持っているため、カメラの位置(position)に反転ベクトルを足し算します。add メソッドは呼び出し元のベクトルに計算結果を反映させるため、跳ね返してくれるというわけです。

mulitplyScalarはベクトルに対して単一の値を掛け算できるメソッドで、これで跳ね返りの距離を調整します。

(あまり小さい数にすると、衝突後もオブジェクトをすり抜けてしまうので注意が必要です)

three.jsのおかげでとっても短いコードで実現できましたね。

全てのコード

 

<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
    <title>Aframe example Stand on Ground</title>
  </head>

  <script type="text/javascript">
    /**
     * 地面に立つためのコンポーネント(前回記事の範囲)
     */
    AFRAME.registerComponent('stand-on', {
      schema: {
        raycaster: { type: 'selector' },
      },
      dependency: ['raycaster'],
      init: function () {
        this.el.addEventListener('raycaster-intersection', (evt) => {
          if (evt.target !== this.data.raycaster) return; // 対応するレイキャスターのみ反応
          this.raycaster = evt.target.components.raycaster;
          this.target_el = evt.detail.els[0];
        });

        this.el.addEventListener('raycaster-intersection-cleared', (evt) => {
          if (evt.target !== this.data.raycaster) return;
          this.raycaster = null;
          this.target_el = null;
        });
      },

      tick: function () {
        if (!this.raycaster || !this.target_el) return;
        const item = this.raycaster.getIntersection(this.target_el);
        this.el.object3D.position.y = item.point.y + 1.7;
      },
    });

    /**
     * 前後左右のヒット(今回記事の範囲)
     */
    AFRAME.registerComponent('wall-collider', {
      schema: {
        raycaster: { type: 'selector' },
      },
      multiple: true,
      dependency: ['raycaster'],
      init: function () {
        this.el.addEventListener('raycaster-intersection', (evt) => {
          console.log(evt.target);
          if (evt.target !== this.data.raycaster) return; // 対応するレイキャスターのみ反応
          this.face = evt.detail.intersections[0].face.normal;
        });

        this.el.addEventListener('raycaster-intersection-cleared', (evt) => {
          if (evt.target.id !== this.data.id) return;
          this.face = null;
        });
      },

      tick: function () {
        if (!this.face) return;
        this.el.object3D.position.add(this.face.multiplyScalar(0.5));
      },
    });
  </script>
  <body>
    <a-scene>
      <a-assets>
        <a-asset-item id="envGlb" src="./Crater.glb"></a-asset-item>
      </a-assets>

      <a-camera
        wall-collider__f="raycaster:#raycaster-f"
        wall-collider__b="raycaster:#raycaster-b"
        wall-collider__r="raycaster:#raycaster-r"
        wall-collider__l="raycaster:#raycaster-l"
        stand-on="raycaster:#stand-on-ray"
      >
        <a-entity id="stand-on-ray" raycaster="objects: .ground; direction:0 -1 0"></a-entity>

        <!-- 四方のレイキャスターを定義 -->
        <a-entity id="raycaster-f" raycaster="objects: .collidable; far: 1; direction:0 0 -1"></a-entity>
        <a-entity id="raycaster-b" raycaster="objects: .collidable; far: 1; direction:0 0 1"></a-entity>
        <a-entity id="raycaster-l" raycaster="objects: .collidable; far: 1; direction:1 0 0"></a-entity>
        <a-entity id="raycaster-r" raycaster="objects: .collidable; far: 1; direction:-1 0 0"></a-entity>
      </a-camera>

      <a-gltf-model position="0 0 0" src="#envGlb" class="ground" animation-mixer=""></a-gltf-model>

      <a-sky color="skyblue"></a-sky>
      <a-box color="red" class="collidable" position="0 1 -4" scale="2 2 2"></a-box>
    </a-scene>
  </body>
</html>