athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

画像システムのリプレイスについて基調講演を行いました

情報システム部の河野です。 入社・開発2年目で、現在はRuby on Rails を使った開発を担当しています。

本日は2017年7月26日(水)に行われました、

ZDNet Japan × TechRepublic Japan & AWS Partner Network 5週間連続セミナーシリーズ データベース編 導入事例から考える–なぜ今、クラウドDBが注目されるのか? の内容について書きたいと思います。

今回このイベントにて、弊社の取り組みについて弊社の鈴木が基調講演をさせて頂きました! 「OracleからAuroraへの移行とオンプレミスとの連携」と題しまして、2016年に行った画像システムのOracle DBのオンプレミス環境からAWSへの移行を行った際の経緯や検討事項、本稼働後の課題についてご紹介しました。

ご存じの方も多いとは思いますが、弊社アットホームは不動産会社と物件を探す方をつなぐ不動産情報サービスを展開しています。 今では物件を探し検討する際、物件の画像がついているのは当たり前とも言える時代です。 弊社の各種サービスでも画像を利用したものは多くあり、その物件画像の配信サービスを支えているシステムは弊社のサービスを支える柱の一つとも言えます。

画像システムの移行と結果

さて、前述にもあります通り画像の需要が増えたことで、画像配信システムのリクエストは1億数千万件/日に上り、キャッシュを利用してもリクエストの件数は増加の傾向にありました。

それにより弊社のシステムには、運用・保守費用の増加や、24時間365日の稼働を行うための安定性確保の課題が浮上してきました。

具体的には

  • Oracleが接続するSANのIO性能の限界
  • ストレージの容量追加やDBサーバのメモリ追加の度に作業が発生(数ヶ月に一度のメンテナンス)
  • ライセンスの上限
  • データ増加に伴いSANの費用が増加(月額課金のサービスを利用)

などです。

そこで2016年7月より、Oracle DBからAmazon Auroraへの移行を行い新システムの運用を開始しました。 弊社がAuroraを採用した理由としては

  • 高可用性
  • 高耐久性
  • 運用の手間が少ない
  • MySQL5.6互換
  • 拡張性

などがあります。

本稼働後の状況

パフォーマンス面では、参照性能はOracle RACとほぼ同等の性能を発揮。大量更新などの際に発生していた、性能劣化が改善しました。

可用性の面では、パラメータチューニングがほとんど不要で、容量追加作業が無くなり、費用面においては、旧システムでかかっていた費用と比較すると半額程度(推定)になりました。

以上のことから、旧システムに比べて随分と楽になった、やってよかった!という結果になりました。

アットホームでは新しい技術を積極的に取り入れるチャレンジをしています。

現在も弊社サービスのリプレイスにて、AWSに移行を伴うプロジェクトも始動中です。

(私もこのプロジェクトに開発メンバーとして参加しています!)

弊社では一緒に開発を行ってくれるエンジニアを募集しています。

興味がある方は下記からエントリお願いします。 ↓↓↓ athome-inc.jp

ASP.NET Coreの環境はASPNETCORE_ENVIRONMENT未設定の場合Productionになる

情報システム部のやまだです。

大分久しぶりに書きますが、これを機にまた書き始めようかな、と。

さて、以前弊社の高野が下記の記事にて

dblog.athome.co.jp

なんでデフォルトがProductionなんだろうか? なにか変な設定が入っているのかな?

と書いていたのですが、その理由も含めてわかったので書き留めておきます。

What happened?

上記にもあるように、アプリケーションの設定をappsettings.jsonに記載してありまして、ステージングや本番など環境ごとにさらに下記のようなファイルを作成していました。

  • ステージング環境(appsettings.staging.json)
  • 本番環境(appsettings.production.json)

appsettingsのあとのファイル名は環境変数のASPNETCORE_ENVIRONMENTの値で決まり、値がstagingならばappsettings.jsonに加えてappsettings.staging.jsonも読み込まれるという、環境変数によって設定を変えるというよくある感じです。RailsだとRAILS_ENV、ExpressだとNODE_ENVですね。

で、ある日、IISで設定したはずのASPNETCORE_ENVIRONMENTの設定が消えていて、気が付いたら本番の設定が適用されていて危機一髪ということがありました。

why?

そもそもIISの設定が無くなったのかはともかくとして、

「そもそもデフォルトってProductionなの?」

「なぜデフォルトがProductionなの?」

ということでちょっと調べて(ググって)みたら見つかりました。

デフォルトってProductionなの?

下記の通りProductionでした。

Hosting/HostingEnvironment.cs at dev · aspnet/Hosting · GitHub

なぜデフォルトだとProductionなの?

TL; DR セキュリティ的によろしくないからProductionにしている

なんでDevelopmentじゃなくてProductionなの?

RailsやExpressはDevelopmentなのに。

ということでさらにググってみたところ、なんかそのものズバリなissueを発見。

Why is hosting environment default value "production"? · Issue #863 · aspnet/Hosting · GitHub

以下、関係ありそうな部分を引用してみます。

blowdart commented on 14 Oct 2016
So, the reason we went with production by default is because both our templates, and in general other people's code add detailed error messages, debug logging, and other things that can cause information disclosure vulnerabilities (at best), or things like auth bypass at worst, depending on what you wrapped in an env.IsDevelopment() check.

By defaulting to production we remove that risk.

In addition launching from inside VS and VS Code sets the environment as development, and best practice generally acknowledges developers should not have every day access to production assets, the risk of defaulting to development is far greater than defaulting to release/production.

 よし、翻訳だ。

ブローダート はコメントしました on 14 Oct 2016
だから、我々がデフォルトでプロダクションを行ったのは、テンプレートと一般的に他の人のコードの両方が、詳細なエラーメッセージ、デバッグログ、および情報漏えいの脆弱性を引き起こす可能性のあるもの、あなたがenv.IsDevelopment()チェックでラップした内容に応じて、最悪です。

デフォルトでは、プロダクションはそのリスクを排除します。

さらに、VSとVSコードの内部からの起動は、開発環境として環境を設定し、開発者が毎回運用資産にアクセスする必要がないことを一般的に認めているため、開発の不履行リスクはリリース/生産のデフォルトよりもはるかに大きくなります。

デバッグ情報とかだだもれしちゃうから安全な方に振っておくよ、ということですね。

ほか、こちらのコメントの方が端的でわかりやすいかもしれません。

DefaultEnvironmentName should be development · Issue #712 · aspnet/Home · GitHub

blowdart commented on 1 Jul 2015
Yes, but your example of web.config is precisely why files don't work. People didn't build in release and publish because it wasn't the default. The amount of debug web sites, with detailed error messages out there is awful. 

ブローダート はコメントしました on 1 Jul 2015
はい。しかし、web.configの例は、ファイルが機能しない理由です。人々はデフォルトではなかったので、リリースでビルドして公開しませんでした。詳細なエラーメッセージが表示されたデバッグWebサイトの量はひどいです。

あー、なるほど。

この辺の方針というか思想というかデザインというかは、言語やフレームワークによって違いそうなので、気を付けたいですね。

ASP.NET Coreのアクセス認可機構

情報システム部の中嶋です。アットホームには2017年4月にJoinし、現在は社内向けの営業支援システムの開発を担当しています。
今日は、ASP.NET Coreが持つアクセス認可の定義とその確認方法について紹介してみたいと思います。


業務システムでは、利用者の役職や役割に応じて利用できる機能、できない機能が存在する場合が一般的です。
ユーザー認証(authentication:いま操作している人がAさんであることを確認する行為)についてはActive Directoryなど外部のシステムに依存することも多いですが、アクセス認可(authorization:Aさんが機能Bを利用しても良いこと)の定義とその確認に関しては各システムが持つ機能に依存するため、それぞれのシステムで用意することが多いと思います。ユーザビリティを考慮するとアクセス認可の確認結果はメニューの表示/非表示で表現されますが*1、URLを直接入力してアクセスされる可能性も考慮するとサーバー上のコントローラークラスにあるアクションメソッドにもアクセス認可を確認する機構が必要となります。
ASP.NET Coreには、この「コントローラークラスのアクションメソッドにおけるアクセス認可の確認」が簡単に行える機構が備わっています。

アクションメソッドの利用を制限する

コントローラークラスのアクションメソッドにおいてアクセス認可の確認を行う方法は非常に簡単で、そのメソッドにAuthorize属性を付けるだけです。メソッドにつけられている場合はそのメソッド、クラスにつけられている場合にはそのクラスに含まれるすべてのメソッドの利用が、許可されたユーザーのみに制限されます。

public class UserController
{
    [Authorize]
    public void ChangePassword(string newPassword)
    {
        // ...
    }
}

例えばパスワード変更のような「許可されたユーザー」が「ユーザー認証されたすべてのユーザー」の場合は引数なしのAuthorize属性使用しますが、「Administratorロールに属しているユーザー」のように特定の条件に制限される場合はAuthorize属性に引数を付けて使用します。

Authorize属性の引数には、ロールを指定するRolesと、ポリシーを指定するPolicyの2つがあります。

ロールを指定して制限する

例えば、Administratorロールに属するユーザーのみが使用可能なアクションメソッドには、Roles引数に「Administrator」というロールの名前を指定します。

public class UserController
{
    [Authorize(Roles = "Administrator")]
    public void GetData(string newPassword)
    {
        // ...
    }
}

こうすることによって、CurrentThreadのCurrentPrincipalがAdministratorロールに含まれている場合のみ、このメソッドを使用できるように制限できます。
またRolesに複数のロールの名前を指定することによって、「いずれかのロールに属しているユーザーが利用できる」という制限を行うこともできます*2

ポリシーを指定して制限する

「ユーザー認証されたユーザーのうち一部のユーザーだけが利用可能だが、その条件はロールではない」という場合には、ポリシーを利用します。
ポリシーを利用する場合には、事前にポリシーを作成したうえで、Authorize属性にPolicy引数を付けて指定します。

public class UserController
{
    [Authorize(Policy = "EmployeeOnly")]
    public void GetData(string newPassword)
    {
        // ...
    }
}

この例では「EmployeeOnly」というポリシーを指定していますが、このポリシーは自分で定義する必要があります。

独自のポリシーを定義する

アクションメソッドの使用を制限する際にロール以外の条件を使用するためには、独自のポリシーを定義する必要があります。
ポリシーはシステム全体に影響する定義ですのでStartupクラスのConfigureServicesメソッドにおいて定義しますが、その方法は判断基準となる属性によって「ロールによって判断するもの」「クレームによって判断するもの」「その他の属性によって判断するもの」の3つに分けられます*3。このうち、「ロールに属していること」と「クレームがアサインされていること」については、簡単にポリシーを定義するために専用のメソッドが用意されています。

ロールを使用してポリシーを定義する

例えばAdministratorロールに属していることを要件とするポリシーは、RequireRoleメソッドを使用して定義し、AddAuthorizationメソッドによってアクセス認可の1つとして登録します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator")));

        // ...
    }
}

クレームを使用してポリシーを定義する

同じようにEmployeeNumberという名前のクレームが存在することを要件とするポリシーは、RequireClaimメソッドを使用して下記のように定義します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber")));

        // ...
    }
}

また、EmployeeNumberというクレームの値が01であることを要件とするポリシーは、下記のように定義します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("SpecialUserOnly", policy => policy.RequireClaim("EmployeeNumber", "01")));

        // ...
    }
}

その他の方法でポリシーを定義する

ロールやクレーム以外の属性を使用してアクセス認可を行う場合についてですが、この場合は、ポリシーとともに「要件」と「その要件を満たすことを判断するための処理(ハンドラー)」を作成する必要があります。

独自の要件を定義する

例えば、「関東の営業担当であること」と言ったポリシーを作成する場合を考えます。
このような場合は、まず「特定地域の営業担当であること」という要件を作成します。
ここでは「要件」と表現していますが、「アクセス認可の判断基準となる評価軸」という方がより正確かもしれません。「特定地域の営業担当であること」という評価軸を作成しておくことにより、「関東の営業担当であること」だけでなく「北海道の営業担当であること」「九州と沖縄の営業担当であること」なども簡単に作成できるようになります。これは「Administrator」というロール名を指定してポリシーを作成するRequireRoleメソッドと同じような考え方です。

public class SalesAreaRequirement : IAuthorizationRequirement
{
    public string[] SalesAreas { get; protected set; }

    public SalesAreaRequirement(string[] areas)
    {
        this.SalesAreas = areas;
    }
}

次にこの要件を使用したポリシーを作成します。この時、要件の引数に「関東」と設定することで、「関東の営業担当であること」というポリシーになります。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("RequireSalesAtKantoArea", policy => policy.Requirements.Add(new SalesAreaRequirement(new string[] { "関東" }))));

        // ...
    }
}

最後に、この要件を実際に確認するハンドラーを作成します。このメソッドの中でcontext.Succeed(requirement)が実行された場合、アクションメソッドに対するアクセスが許可されます。

public class SalesAreaHandler : AuthorizationHandler<SalesAreaRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SalesAreaRequirement requirement)
    {
        // ここでDBなどからログインユーザーの担当地域を取得しておき、salesAreaに格納しておく。

        if (requirement.SalesAreas.Contains(salesArea))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        return Task.CompletedTask;
    }
}

また、このハンドラーはASP.NET Coreのフレームワークによってパイプラインの一部としてインジェクションされるため、その定義も必要となります。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddSingleton<IAuthorizationHandler, SalesAreaHandler>();

        // ...
    }
}

このように、ASP.NET Coreではアクセス認可をポリシーとそのポリシーを満たすための要件として表現することで、容易にわかりやすく定義することができます。
また、ここで作成したポリシーはRazor構文の中でも使用できるため、アクションメソッドのアクセス認可と同じ条件でメニュー等の表示/非表示を制御することもできます。


ということで、今回はASP.NET Coreが持つアクセス認可の定義とその確認方法について紹介してみました。
アクセス認可などは比較的「車輪の再発明」をしがちな分野かと思いますが、用意されているものをうまく利用することで、全体としてより大きな価値が提供できていければ良いなぁと考えています。

*1:Aさんが参照しても良いデータ、参照できないデータの判断もアクセス認可の一部ですが、今回は機能だけに注目して話を進めていますので、メニューでの表現が中心になります。

*2:Rolesに複数のロールを指定すれば「or」ですが、Authorize属性自体を複数つけると「and」になります。

*3:ロールによる判断はポリシーを定義せずともAuthorize属性のRoles引数で可能ですが、「メニューの表示/非表示の制御」のようにRazor構文の中で使用しようとするとポリシーが必要となります。

HoloLensでオブジェクトを回転してみる

こんにちは、情報システム部の高野です。
ちょっと間が空いてしまいましたが
拡大縮小に引き続き、今度は回転です。
今回は、Y軸に対する回転だけやります。
(いまのところそれしか使わなそうなので)

準備

前回の拡大縮小で作成したプロジェクトにそのまま追加していきます。

回転用のハンドルCubeとスクリプトの作成

回転ハンドルを作成する

HierarchyウィンドウのCube内のWireを右クリックし
「3D Obejct」→「Sphere」を選択します。
名称をRotateHandle1に変更します。
サイズと位置を変更します。
Position X=0.5 Y=0 Z=-0.5
Size X/Y/Z=0.05
前回作成したHandle用のマテリアルをアタッチします。

スクリプトを作成する

Projectウィンドウで「C# Script」を作成し
名称をRotateControllerに変更します。
これをRotateHandle1オブジェクトにアタッチします。

RotateController.csをVisualStudioで開き以下の様に修正します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class RotateController : MonoBehaviour, IInputHandler, ISourceStateHandler
{
    private bool isHold;
    private IInputSource currentInputSource;
    private uint currentInputSourceId;
    private Vector3 prevPos;
    private GameObject targetObj;

    // Use this for initialization
    void Start()
    {
        // rootから取れば良かった
        targetObj = transform.root.gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        if (!isHold) return;

        Vector3 handPos;
        currentInputSource.TryGetPosition(currentInputSourceId, out handPos);
        // ワールド空間からローカル空間に変換
        handPos = Camera.main.transform.InverseTransformDirection(handPos);

        var diff = prevPos - handPos;
        prevPos = handPos;

        targetObj.transform.Rotate(0f, diff.x * 360, 0f, Space.World);
    }

    public void OnInputUp(InputEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnInputDown(InputEventData eventData)
    {
        if (!eventData.InputSource.SupportsInputInfo(eventData.SourceId, SupportedInputInfo.Position))
            return;

        if (isHold) return;

        isHold = true;
        InputManager.Instance.PushModalInputHandler(gameObject);

        currentInputSource = eventData.InputSource;
        currentInputSourceId = eventData.SourceId;

        currentInputSource.TryGetPosition(currentInputSourceId, out prevPos);
        prevPos = Camera.main.transform.InverseTransformDirection(prevPos);
    }

    public void OnSourceDetected(SourceStateEventData eventData)
    {
    }

    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }
}

TransformクラスのInverseTransformDirectionメソッドで
ワールド空間からローカル空間に変換してますが
オブジェクトの正面からだけ操作するのであればこれは必要ありません。
オブジェクトの横や後ろに回って操作すると
これが無いと手の動きと回転が合わなくなります。
下図のような位置関係の場合は、手を横に動かすとZ方向に動かすことになります。
このままだとXの移動距離では無くZの移動距離が必要になります。
f:id:taktak1974:20170620160225p:plain
自分とオブジェクトの位置関係に応じて
Xの移動距離だったりZの移動距離だったりに変更するのは大変なので
カメラのローカル空間に変換して手の横移動は常にXの移動にしました。
これがよくわからずに嵌りました(Unity初心者なので色々、嵌ります)


後は、各辺にハンドルをコピーします。
完成したものがこちらになります。

hololens rotate object


これを作っている間にMRDesignLabs_Unityが公開されて
作る必要なかったなって感じにはなったのですが
勉強にはなったので良かった。
MRDesignLabs_Unityは、HoloToolkitのイベントを使ってないので
やり方は結構違いますけどね。*1


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
Unityできる人もいるとうれしいな
athome-inc.jp

*1:MRDesignLabs_UnityだとHoloLens以外でも動かせそう

HoloLensでオブジェクトを変倍してみる

こんにちは、情報システム部の高野です。
今回は、HoloLensでオブジェクトの拡大縮小をします。
しかも等倍ではなくXYZ軸自由に拡大縮小できるように作ります。

HoloLensで拡大縮小する方法は、いくつかあります。
・声で指示する方法(以前テレビでやってました)
・HoloLensにプリインストールされているActiongramの様にスライダーのようなもので操作する方法
・下記ブログのようにワイヤーフレームにハンドルを付けて操作する方法
こちらのブログを大分参考にさせていただきました)
blog.d-yama7.com

今回は、変倍ということもあり3番目の方法でやってみます。


準備

まずプロジェクトの準備は、以前と同様です。
HoloToolkitをimportしプロジェクトの設定をします。
HololensCamera・InputManager・DefaultCursorをHierarchyウィンドウに配置します。
f:id:taktak1974:20170606101603p:plain

拡大縮小するオブジェクトとワイヤーフレームの配置

Cubeの配置

まず拡大縮小するオブジェクトを配置します。
これは何でも良いのですが、今回はCubeを配置します。
Hierarchyウィンドウで右クリックし「3D Object」→「Cube」を選択して
Cubeオブジェクトを配置します。
配置されたCubeを選択しInspectorウィンドウで位置と大きさを変更します。
Position Z=1
Scale X/Y/Z=0.2

適当にMaterialを用意しCubeに色を付けておきます。
ワイヤーフレームを緑にするのでそれ以外の色がいいです)

ワイヤーフレームの配置

ワイヤーフレーム用のShaderを作成します。
ProjectウィンドウのAssetsフォルダ上で右クリックし
「Create」→「Shader」→「Standard Surface Shader」を選択します。
作成されたShaderの名前をWireに変更しておきます。
Wireシェーダーを右クリックし「Open C# Project」を選択します。
VisualStudioでWire.shaderを開きこちらを参考に編集します。

次にMaterialを作成します。
ProjectウィンドウのAssetsフォルダ上で右クリックし
「Create」→「Material」を選択します。
Materialの名称をWireに変更します。
Wireマテリアルを選択しInspectorウィンドウの上部にある
Shaderプルダウンから「Custom」→「Wire」を選択します。
LineColorを緑に変更します。
f:id:taktak1974:20170606105725p:plain

HierarchyウィンドウのCubeを右クリックし子オブジェクトとしてCubeを作成します。
名称をWireに変更しておきます。

WireマテリアルをWireオブジェクトに関連づけます。
ここまでで下図のようになります。
f:id:taktak1974:20170606110049p:plain

拡大縮小用のハンドルCubeとスクリプトの作成

リサイズハンドルを作成する

HierarchyウィンドウのWireオブジェクトを右クリックし
「3D Object」→「Cube」を選択します。
名称をResizeHandle1に変更します。
Position X=0.5 Y=0.5 Z=-0.5
Size X/Y/Z=0.05
に変更します。
緑色のマテリアルを作成しResizeHandle1に関連付けます。

これを各頂点にコピーすることになるのですが
動きを付けてからにします。

スクリプトを作成する

Projectウィンドウで「C# Script」を作成し
名称をResizeControllerに変更します。
これをResizeHandle1オブジェクトに関連付けておきます。

ResizeController.csをVisualStudioで開き以下の様に修正します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ResizeController : MonoBehaviour, IInputHandler, ISourceStateHandler
{
    private GameObject targetObj;
    private bool isHold;
    private IInputSource currentInputSource;
    private uint currentInputSourceId;
    private Vector3 startHandPos;
    private Vector3 axisVect;
    private Vector3 startScale;

    // Use this for initialization
    void Start()
    {
        // 本来は外から設定した方が良いです
        targetObj = transform.parent.parent.gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        if (!isHold) return;

        Vector3 handPos;
        currentInputSource.TryGetPosition(currentInputSourceId, out handPos);

        var diff = handPos - startHandPos;

        // 各軸ごとの方向を取得
        var xVect = axisVect.x >= 0 ? 1 : -1;
        var yVect = axisVect.y >= 0 ? 1 : -1;
        var zVect = axisVect.z >= 0 ? 1 : -1;

        targetObj.transform.localScale = startScale + new Vector3(diff.x * xVect, diff.y * yVect, diff.z * zVect);
    }

    public void OnInputDown(InputEventData eventData)
    {
        if (!eventData.InputSource.SupportsInputInfo(eventData.SourceId, SupportedInputInfo.Position))
            return;

        if (isHold) return;

        isHold = true;
        InputManager.Instance.PushModalInputHandler(gameObject);

        currentInputSource = eventData.InputSource;
        currentInputSourceId = eventData.SourceId;

        currentInputSource.TryGetPosition(currentInputSourceId, out startHandPos);

        startScale = targetObj.transform.localScale;

        axisVect = transform.position - targetObj.transform.position;
    }

    public void OnInputUp(InputEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnSourceDetected(SourceStateEventData eventData)
    {
    }

    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }
}

今回は、以前の移動時と違い手の位置を取れるように
IInputHandler、ISourceStateHandlerを使いました。
こちらの方が簡単でした。

あとは、各頂点にリサイズハンドルをコピーすればOKです。
最終的なHierarchyウィンドウは下図になります。
f:id:taktak1974:20170606141432p:plain

ここまでで拡大縮小が可能です。

対角点を基点にする

上記コードだと中心点が基点になって拡大縮小されますが
対角点を基点にしたいと思います。

以下のようにResizeController.csを変更します。

    void Update()
    {
        if (!isHold) return;

        ・・・

        targetObj.transform.localScale = startScale + new Vector3(diff.x * xVect, diff.y * yVect, diff.z * zVect);

        // 追加
        var scaleDiff = targetObj.transform.localScale - startScale;
        targetObj.transform.position = startPos + new Vector3(scaleDiff.x * xVect, scaleDiff.y * yVect, scaleDiff.z * zVect) / 2;
    }

    public void OnInputDown(InputEventData eventData)
    {
        ・・・

        startScale = targetObj.transform.localScale;
        // 追加
        startPos = targetObj.transform.position;

        axisVect = transform.position - targetObj.transform.position;
    }

拡大縮小しつつ同じ方向に移動しているので基点が対角点に見えます。
(もっと良い方法あるんですかね?)


それで今回作成したものはこんな感じに動きます。

hololens resize object




ワイヤーフレームやハンドルが一緒に拡大縮小しちゃうのが玉に瑕ですね。
課題は残しつつも一応変倍ができるようになりました。
後は回転ができれば基本的なオブジェクトの操作はできるようになりますね


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp

HoloLensでオブジェクトを移動してみる

こんにちは、情報システム部の高野です。
今回は、オブジェクトの移動を試してみます。

前回作ったプロジェクトをそのまま利用します。


HoloLensでオブジェクト移動するために色々と調べてみたところ
いくつかの実装方法があることが分かりました。
今回は、そのうちの2つの方法を試してみます。
1つは、UnityのInteractionManagerのイベントを使う方法
もう1つは、HoloToolkitのIManipulationHandlerを実装する方法です。

InteractionManagerを利用した移動

前回、オブジェクトを生成できるようにしたので
その生成したオブジェクトを移動できるようにしていきます。

移動用のスクリプトを作成する

ProjectウィンドウのScriptsフォルダにGestureControllerというスクリプトを作成します。
作成してGestureControllerHierarchyウィンドウのObjectManager
ドラッグ&ドロップし設定します。

CubeプレハブにTagを設定する

後程、別オブジェクトを作成するので区別できるようにTagを設定しておきます。
ProjectウィンドウのPrefabsフォルダからCubeを選択します。
Inspectorウィンドウの上部にTagプルダウンがあるので「Add Tag」を選択します。
TagsにあるプラスボタンをクリックしTag名をInteractionとし保存します。
もう一度CubeプレハブのInspectorウィンドウを開き
Tagで「Interaction」を選択します。

Tagに設定した状態
f:id:taktak1974:20170522160017p:plain

移動処理を実装する

GestureControllerスクリプトをVisualStudioで開きます。
コードを以下の様に変更します

using HoloToolkit.Unity.InputModule;
using UnityEngine;
using UnityEngine.VR.WSA.Input;

public class GestureController : MonoBehaviour 
{

    private Vector3 prevPos;
    private bool isHold;
    private GameObject focusObj;

    // Use this for initialization
    void Start()
    {
        InteractionManager.SourcePressed += InteractionManager_SourcePressed;
        InteractionManager.SourceReleased += InteractionManager_SourceReleased;
        InteractionManager.SourceLost += InteractionManager_SourceLost;
        InteractionManager.SourceUpdated += InteractionManager_SourceUpdated;
    }

    // Update is called once per frame
    void Update()
    {
        var obj = GazeManager.Instance.HitObject;

        // ホールドしている時は、オブジェクト入れ替えない
        if (obj != null && !isHold)
        {
            // TagがInteractionのものだけを対象とする
            if (obj.tag == "Interaction")
            {
                focusObj = obj;
            }
        }
    }

    private void InteractionManager_SourcePressed(InteractionSourceState state)
    {
        if (focusObj == null) return;

        focusObj.GetComponent<Rigidbody>().useGravity = false;

        Vector3 handPosition;
        if (state.source.kind == InteractionSourceKind.Hand && 
            state.properties.location.TryGetPosition(out handPosition))
        {
            isHold = true;
            prevPos = handPosition;
        }
    }

    private void InteractionManager_SourceReleased(InteractionSourceState state)
    {
        if (focusObj == null) return;

        focusObj.GetComponent<Rigidbody>().useGravity = true;
        isHold = false;
        focusObj = null;
    }

    private void InteractionManager_SourceLost(InteractionSourceState state)
    {
        if (focusObj == null) return;

        focusObj.GetComponent<Rigidbody>().useGravity = true;
        isHold = false;
        focusObj = null;
    }

    private void InteractionManager_SourceUpdated(InteractionSourceState state)
    {
        if (!isHold || focusObj == null) return;

        Vector3 handPosition;
        state.properties.location.TryGetPosition(out handPosition);

        if (state.source.kind == InteractionSourceKind.Hand && 
            state.properties.location.TryGetPosition(out handPosition))
        {
            var moveVector = Vector3.zero;
            moveVector = handPosition - prevPos;

            prevPos = handPosition;

            var handDistance = Vector3.Distance(Camera.main.transform.position, handPosition);
            var objectDistance = Vector3.Distance(Camera.main.transform.position, focusObj.transform.position);

            focusObj.transform.position += (moveVector * (objectDistance / handDistance));
        }
    }
}

InteractionManager_SourcePressedで手の位置を取得しておき
InteractionManager_SourceUpdatedで移動距離を計算して
オブジェクトに新しい位置を割り当てます。
この移動方法は下記の記事を参考にさせていただきました。
qiita.com
もうちょっと複雑な計算をしているものも見かけたのですが
今のところは、この方法で支障がないような気がしています。

IManipulationHandlerを利用した移動

今度はHoloToolkitのIManipulationHandlerインターフェイスを実装する方法です。
HoloToolkitを利用しているので、できればこちらの方法で実装した方が良いかと思います。

新たなCubeプレハブを作成する

元々、用意してあったCubeとは別のオブジェクトを生成して
そちらをIManipulationHandlerで移動するようにしていきます。
前回、作成したようにCubeのプレハブを作成します。
見た目で判別できるように黄色にしておきます。
プレハブ名もYellowCubeにします。
RigidbodyCubeManagerスクリプトも設定しておきます。

YelloCubeを生成できるようにコードを変更する

前回作成したObejectManagerスクリプトのコードを変更します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ObjectManager : MonoBehaviour, IInputClickHandler
{
    public GameObject obj1;
    public GameObject obj2;
    private bool toggle = true;

    private void Start()
    {
        InputManager.Instance.PushFallbackInputHandler(gameObject);
    }

    public void OnInputClicked(InputClickedEventData eventData)
    {
        var obj = toggle ? obj1 : obj2;
        toggle = !toggle;

        var pos = Camera.main.transform.position;
        var forword = Camera.main.transform.forward;

        Instantiate(obj, pos + forword, new Quaternion());
    }
}

Unityに戻りobj1にCubeプレハブをobj2にYellowCubeプレハブを設定します。

移動処理を実装する

Unityで新しいスクリプトManipulationControllerを作成します。
作成したスクリプトYellowCubeプレハブに設定します。

設定したスクリプトをVisualStudioで開き
下記の様にコードを変更します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ManipulationController : MonoBehaviour, IManipulationHandler
{

    Vector3 prevPos;

    public void OnManipulationCanceled(ManipulationEventData eventData)
    {
        GetComponent<Rigidbody>().useGravity = true;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnManipulationCompleted(ManipulationEventData eventData)
    {
        GetComponent<Rigidbody>().useGravity = true;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnManipulationStarted(ManipulationEventData eventData)
    {
        GetComponent<Rigidbody>().useGravity = false;
        prevPos = eventData.CumulativeDelta;

        // これが無いとオブジェクトにフォーカス時しか操作ができない
        InputManager.Instance.PushModalInputHandler(gameObject);
    }

    public void OnManipulationUpdated(ManipulationEventData eventData)
    {
        Vector3 moveVector = Vector3.zero;
        moveVector = eventData.CumulativeDelta - prevPos;

        prevPos = eventData.CumulativeDelta;

        // 手の位置が取得できないので決め打ちで40cmに
        var handDistance = 0.4f;
        var objectDistance = Vector3.Distance(Camera.main.transform.position, gameObject.transform.position);

        gameObject.transform.position += (moveVector * (objectDistance / handDistance));
    }
}

移動のロジックは、InteractionManagerの方とあまり変わりはありませんが
IManipulationHandlerだと元記事にもありますが手の位置の取得方法が不明です。
私もいろいろと試しましたが今のところ分かっていません。
下記を実行するとfalseが返ってくるので取得できる望みは低いのはないかと思います。

eventData.InputSource.SupportsInputInfo(eventData.SourceId, SupportedInputInfo.Position)

OnManipulationUpdatedメソッドは、InteractionManager.SourceUpdatedイベントと違い
フォーカスが外れるとイベントが飛びません。
それを回避するためにInputManager.Instance.PushModalInputHandlerメソッドに
gameObjectを渡しています。


今回、作成したものはこんな感じで動きます。

HoloLensでオブジェクトを移動する
動画では分かりづらいのですが、
緑のオブジェクト(InteractionManagerを使った方)の方が
手の移動とオブジェクトの移動がしっくりきます。



HoloToolkitを利用してオブジェクトを移動するなら
IManipulationHandlerを実装する方法でやりたいところですが
手の位置の取得方法が分からないのでうーんって感じです。
(やり方分かる方は、こそっと教えてほしいです)
今回は以上になります。次もHoloLensネタになるかと思います。


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp

HoloLensで新しいオブジェクトを生成してみる

こんにちは、情報システム部の高野です。
前回は、起動時にすでに存在するオブジェクトへの操作を試しましたが
今回は、新しくオブジェクトを生成する方法を試してみました。

前回の「カメラを変更する」までは手順は同じなので省略します。

カーソルとジェスチャーの設定をする

前回と同様ですが、InputManagerDefaultCursor
Hierarchyウィンドウにドラッグ&ドロップしておきます。
ここまででHierarchyウィンドウは下図のようになります。
f:id:taktak1974:20170515114532p:plain

3Dオブジェクトのプレハブを用意する

新しいオブジェクトを生成するには生成元のプレハブを作成しておく必要があります。*1
Hierarchyウィンドウの「Create」メニューから
「3D Object」→「Cube」を選択します。
InspectorウィンドウでScaleをすべて0.1に変更しておきます。

オブジェクトに色を付ける

ProjectウィンドウのAssetsフォルダにMaterialsというフォルダを作成します。
Materialsフォルダ内で右クリックし「Create」→「Material」を選択します。
今回は名前をGreenにしInspectorウィンドウの「Albedo」を緑っぽくしておきます。
作成したMaterialをHierarchyウィンドウ内のCubeにドラッグ&ドロップします。

オブジェクトに重力を与える

Cubeを選択した状態でInspectorウィンドウを開きます。
「Add Component」ボタンをクリックしRigidbodyを検索して選択します。

プレハブ化する

ProjectウィンドウのAssetsフォルダにPrefabsフォルダを作成します。
作成したPrefabsフォルダにHierarchyウィンドウからCubeをドラッグ&ドロップします。
これでCubeプレハブが作成されました。
HierarchyウィンドウからはCubeを削除しておきます。

動的に3Dオブジェクトを生成する

空のGameObjectを作成する

Hierarchyウィンドウの「Create」メニューから「Create Empty」を選択し空のGameObjectを作成します。
作成したGameObjectは名称をObjectManagerに変更しておきます。

オブジェクト生成用のスクリプトを作成する

ProjectウィンドウのAssetsフォルダにScriptsフォルダを作成します。
作成したScriptsフォルダの中にC#Scriptを作成し名称をObjectManagerに変更します。
作成したObjectManagerスクリプトObjectManagerオブジェクトに関連付けます。
(ドラッグ&ドロップでもInspectorからAdd ComponentでもOKです)
InspectorウィンドウでObjectManagerスクリプトを右クリックし「Edit Script」でVisualStudioを起動します。
コードを以下のように変更します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ObjectManager : MonoBehaviour, IInputClickHandler
{
    public GameObject obj;

    private void Start()
    {
        InputManager.Instance.PushFallbackInputHandler(gameObject);
    }

    public void OnInputClicked(InputClickedEventData eventData)
    {
        var pos = Camera.main.transform.position;
        var forword = Camera.main.transform.forward;

        Instantiate(obj, pos + forword, new Quaternion());
    }
}

Instantiateメソッドで指定したオブジェクトを生成することができます。
生成する座標はどの向きを向いていても目の前に落としたかったので
上記のようにしてみました。
(もっといい方法もあるのかもしれませんが、Unity力0なので分かってません。)
このままではフィールドobjがnullのままなのでUnityに戻って設定します。
InspectorウィンドウにObjectManagerを表示します。
ObjectManagerスクリプトに「Obj」というプロパティが増えていると思います。*2
f:id:taktak1974:20170515134246p:plain
そこに作成しておいたCubeプレハブをドラッグ&ドロップします。
下図のようになります。
f:id:taktak1974:20170515134353p:plain


最後に床が検知できるようにSpatialMappingHierarchyウィンドウにドラッグ&ドロップします。
ここまでできたらビルドして実行してみましょう。
※ビルドから実行の流れも前回の記事を見てください。

うまくいっていればAirTapするとCubeが目の前に現れて下に落ちます。
こんな感じで足元にCubeが散らかります。
f:id:taktak1974:20170515143411j:plain

ちょっと動きを付けてみる

このままだと目の前にオブジェクトが生成された瞬間に下に落ちてしまいます。
ちょっと味気ないので少し改良してみます。

UnityでCubeプレハブを選択しInspectorウィンドウを開きます。
現状、Rigidbodyの「Use Gravity」にチェックが入っているので外します。
ProjectウィンドウのScriptsフォルダにCubeManagerというC#スクリプトを作成します。
CubeプレハブのInspectorで「Add Component」からCubeManagerスクリプトを選択します。
スクリプトをVisualStudioで開いて以下のように変更します。

using System;
using UnityEngine;

public class CubeManager : MonoBehaviour
{
    DateTime? createTime;

    // Use this for initialization
    void Start()
    {
        createTime = DateTime.Now;
    }

    // Update is called once per frame
    void Update()
    {
        if (createTime != null)
        {
            if ((DateTime.Now - createTime.Value).Seconds > 1)
            {
                GetComponent<Rigidbody>().useGravity = true;
                createTime = null;
            }
            else
            {
                var qua = gameObject.transform.rotation;
                qua.z += 0.1f;
                gameObject.transform.rotation = qua;
            }
        }
    }
}

1秒間は目の前に浮いているようにしています。
浮いている間は、z方向に回転します。


最終的にはこのような感じになります。

20170515holo


今回は以上です。
次回はできればオブジェクトを掴んで移動に挑戦したいです。(できればね)



弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp

*1:他の方法もあるかもしれませんが、今のところこの方法しか分かっていないです

*2:VisualStudioで保存しておかないと表示されないので注意が必要です