Polymer を使ってライトセーバーを作成する

ライトセーバーのスクリーンショット

概要

Polymer を使用して、モバイル制御された高パフォーマンスの WebGL を構築した方法 モジュール式で設定可能なライトセーバー。重要な点を確認します プロジェクト https://lightsaber.withgoogle.com/ 次回スペースに遭遇したとき、時間を節約できます。 怒っているストームトルーパー。

概要

Polymer や WebComponents が何であるかわからない場合は、まず実際のプロジェクトの抜粋を共有することをおすすめします。これはプロジェクトのランディング ページから抜粋したサンプルです https://lightsaber.withgoogle.com.です。 これは通常の HTML ファイルですが、次のような魔法が加えられています。

<!-- Element-->
<dom-module id="sw-page-landing">
    <!-- Template-->
    <template>
    <style>
        <!-- include elements/sw/pages/sw-page-landing/styles/sw-page-landing.css-->
    </style>
    <div class="centered content">
        <sw-ui-logo></sw-ui-logo>
        <div class="connection-url-wrapper">
        <sw-t key="landing.type" class="type"></sw-t>
        <div id="url" class="connection-url">.</div>
        <sw-ui-toast></sw-ui-toast>
        </div>
    </div>
    <div class="disclaimer epilepsy">
        <sw-t key="disclaimer.epilepsy" class="type"></sw-t>
    </div>
    <sw-ui-footer state="extended"></sw-ui-footer>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-page-landing.js"></script>
</dom-module>

昨今では、モデルの作成に使用する選択肢は HTML5 ベースのアプリケーションですAPI、フレームワーク、ライブラリ、ゲームエンジンなど どのような選択肢があるにしても、うまく組み合わせるセットアップが難しい 高パフォーマンスのグラフィックス制御と、クリーンなモジュラー 構成とスケーラビリティに重点を置いていますそこで Polymer は 低レベルのパフォーマンスも許容します。 プロジェクトを分割して慎重に検討し コンポーネントに分解して Polymer の機能を最大限に活用します。

ポリマーによるモジュール性

Polymer は、 プロジェクトの構築方法を大幅に改善できます。 これを使用すると、サーバー モジュールに含まれているスタンドアロンの完全に機能するモジュールを 作成します。構造(HTML マークアップ)だけでなく、 インラインスタイルとロジックを使用できます。

下記の例をご覧ください。

<link rel="import" href="bower_components/polymer/polymer.html">

<dom-module id="picture-frame">
    <template>
    <!-- scoped CSS for this element -->
    <style>
        div {
        display: inline-block;
        background-color: #ccc;
        border-radius: 8px;
        padding: 4px;
        }
    </style>
    <div>
        <!-- any children are rendered here -->
        <content></content>
    </div>
    </template>

    <script>
    Polymer({
        is: "picture-frame",
    });
    </script>
</dom-module>

しかし、大規模なプロジェクトでは、これら 3 つの論理的思考を コンポーネント(HTML、CSS、JS)をコンパイルし、コンパイル時にのみマージします。まず、 プロジェクトの各要素に独自のフォルダを用意しました。

src/elements/
|-- elements.jade
`-- sw
    |-- debug
    |   |-- sw-debug
    |   |-- sw-debug-performance
    |   |-- sw-debug-version
    |   `-- sw-debug-webgl
    |-- experience
    |   |-- effects
    |   |-- sw-experience
    |   |-- sw-experience-controller
    |   |-- sw-experience-engine
    |   |-- sw-experience-input
    |   |-- sw-experience-model
    |   |-- sw-experience-postprocessor
    |   |-- sw-experience-renderer
    |   |-- sw-experience-state
    |   `-- sw-timer
    |-- input
    |   |-- sw-input-keyboard
    |   `-- sw-input-remote
    |-- pages
    |   |-- sw-page-calibration
    |   |-- sw-page-connection
    |   |-- sw-page-connection-error
    |   |-- sw-page-error
    |   |-- sw-page-experience
    |   `-- sw-page-landing
    |-- sw-app
    |   |-- bower.json
    |   |-- scripts
    |   |-- styles
    |   `-- sw-app.jade
    |-- system
    |   |-- sw-routing
    |   |-- sw-system
    |   |-- sw-system-audio
    |   |-- sw-system-config
    |   |-- sw-system-environment
    |   |-- sw-system-events
    |   |-- sw-system-remote
    |   |-- sw-system-social
    |   |-- sw-system-tracking
    |   |-- sw-system-version
    |   |-- sw-system-webrtc
    |   `-- sw-system-websocket
    |-- ui
    |   |-- experience
    |   |-- sw-preloader
    |   |-- sw-sound
    |   |-- sw-ui-button
    |   |-- sw-ui-calibration
    |   |-- sw-ui-disconnected
    |   |-- sw-ui-final
    |   |-- sw-ui-footer
    |   |-- sw-ui-help
    |   |-- sw-ui-language
    |   |-- sw-ui-logo
    |   |-- sw-ui-mask
    |   |-- sw-ui-menu
    |   |-- sw-ui-overlay
    |   |-- sw-ui-quality
    |   |-- sw-ui-select
    |   |-- sw-ui-toast
    |   |-- sw-ui-toggle-screen
    |   `-- sw-ui-volume
    `-- utils
        `-- sw-t

各要素のフォルダの内部構造は同じで、 ディレクトリとファイルで、ロジック(コーヒー ファイル)、スタイル(scss ファイル)、 ダウンロードします。

sw-ui-logo 要素の例を次に示します。

sw-ui-logo/
|-- bower.json
|-- scripts
|   `-- sw-ui-logo.coffee
|-- styles
|   `-- sw-ui-logo.scss
`-- sw-ui-logo.jade

.jade ファイルを調べてみると、次のようになっています。

// Element
dom-module(id='sw-ui-logo')

    // Template
    template
    style
        include elements/sw/ui/sw-ui-logo/styles/sw-ui-logo.css

    img(src='[[url]]')

    // Polymer element script
    script(src='scripts/sw-ui-logo.js')

スタイルを指定することで、整理整頓されていることがわかりやすくなります。 分離する必要がありますスタイルを Polymer に含めるには 要素では Jade の include ステートメントを使用しているため、実際のインライン CSS があります。 コンパイル後、ファイルの内容が読み取られます。sw-ui-logo.js スクリプト要素は実行時に実行されます。

Bower を使用したモジュラー依存関係

通常、ライブラリやその他の依存関係はプロジェクト レベルで管理します。 ただし、上記の設定では、bower.json が 要素のフォルダ: 要素レベルの依存関係。このアプローチの背景にある考え方 異なる複数の要素を持つ状況で 必要な依存関係だけを読み込むことができます。 見てみましょう。要素を削除する場合は bower.json ファイルも削除するため、その依存関係を削除します。 依存関係を宣言します。各要素が独立して 依存関係が含まれます。

ただし、依存関係の重複を避けるために、.bowerrc ファイルを含めています。 各要素のフォルダ内にあります。保存場所を指定します 同じプロジェクトの最後に 1 つだけ ディレクトリ:

{
    "directory" : "../../../../../bower_components"
}

これにより、複数の要素が THREE.js を依存関係として宣言した場合、bower が最初の要素用に THREE.js をインストールし、2 番目の要素の解析を開始すると、この依存関係がすでにインストールされていることが認識され、再ダウンロードや重複は行われなくなります。同様に、この依存関係は 少なくとも 1 つの要素がファイルに含まれている必要があります。 その bower.json

bash スクリプトは、ネストされた要素構造内のすべての bower.json ファイルを検索します。 次に、これらのディレクトリに 1 つずつ入って、bower install を実行します。 それぞれ:

echo installing bower components...
modules=$(find /vagrant/app -type f -name "bower.json" -not -path "*node_modules*" -not -path "*bower_components*")
for module in $modules; do
    pushd $(dirname $module)
    bower install --allow-root -q
    popd
done

新しいクイック要素のテンプレート

新しい要素を作成するたびに、フォルダと基本的なファイル構造が正しい名前で生成されるため、時間がかかります。そこで、 Slush: シンプルな要素生成ツールを記述します。

このスクリプトはコマンドラインから呼び出すことができます。

$ slush element path/to/your/element-name

ファイル構造とコンテンツがすべて含まれた新しい要素が作成されます。

要素ファイルのテンプレートを定義しました。たとえば、.jade ファイル テンプレート 次のようになります。

// Element
dom-module(id='<%= name %>')

    // Template
    template
    style
        include elements/<%= path %>/styles/<%= name %>.css

    span This is a '<%= name %>' element.

    // Polymer element script
    script(src='scripts/<%= name %>.js')

Slush ジェネレータは、変数を実際の要素のパスと名前に置き換えます。

Gulp を使用して要素を作成する

Gulp はビルドプロセスを制御しています。Google の構造では 以下の手順で Gulp が必要とする要素を特定しました。

  1. 要素をコンパイルして.coffee 個のファイルを .js に保存
  2. 要素をコンパイルして.scss 個のファイルを .css に保存
  3. 要素をコンパイルして.jade ファイルを .html に追加し、.css ファイルを埋め込みます。

詳細は次のとおりです。

要素のコンパイル.coffee 個のファイルを .js に保存

gulp.task('elements-coffee', function () {
    return gulp.src(abs(config.paths.app + '/elements/**/*.coffee'))
    .pipe($.replaceTask({
        patterns: [{json: getVersionData()}]
    }))
    .pipe($.changed(abs(config.paths.static + '/elements'), {extension: '.js'}))
    .pipe($.coffeelint())
    .pipe($.coffeelint.reporter())
    .pipe($.sourcemaps.init())
    .pipe($.coffee({
    }))
    .on('error', gutil.log)
    .pipe($.sourcemaps.write())
    .pipe(gulp.dest(abs(config.paths.static + '/elements')));
});

ステップ 2 と 3 では、gulp とコンパス プラグインを使用して scss をコンパイルし、 .css.jade.html に追加します(上記の 2 と同様のアプローチ)。

ポリマー要素を含める

Polymer 要素を実際に含めるには、HTML インポートを使用します。

<link rel="import" href="elements.html">

<!-- Polymer -->
<link rel="import" href="../bower_components/polymer/polymer.html">

<!-- Custom elements -->
<link rel="import" href="sw/sw-app/sw-app.html">
<link rel="import" href="sw/system/sw-system/sw-system.html">
<link rel="import" href="sw/system/sw-routing/sw-routing.html">
<link rel="import" href="sw/system/sw-system-version/sw-system-version.html">
<link rel="import" href="sw/system/sw-system-environment/sw-system-environment.html">
<link rel="import" href="sw/pages/sw-page-landing/sw-page-landing.html">
<link rel="import" href="sw/pages/sw-page-connection/sw-page-connection.html">
<link rel="import" href="sw/pages/sw-page-calibration/sw-page-calibration.html">
<link rel="import" href="sw/pages/sw-page-experience/sw-page-experience.html">
<link rel="import" href="sw/ui/sw-preloader/sw-preloader.html">
<link rel="import" href="sw/ui/sw-ui-overlay/sw-ui-overlay.html">
<link rel="import" href="sw/ui/sw-ui-button/sw-ui-button.html">
<link rel="import" href="sw/ui/sw-ui-menu/sw-ui-menu.html">

本番環境用に Polymer 要素を最適化する

大規模なプロジェクトでは、多くの Polymer 要素が含まれることになります。プロジェクトでは 50 を超える項目があります。各要素に 別の .js ファイルがあり、一部のライブラリが参照されている場合、 100 個の個別のファイル。つまり ブラウザは多数のリクエストを パフォーマンスの低下につながりますプロセスを連結して圧縮する場合と同様に、 Angular ビルドに適用する場合は、Angular で Polymer プロジェクトを「加速度」 おすすめします

Vulcanize は、依存関係ツリーを単一の HTML ファイルにフラット化し、リクエスト数を削減する Polymer ツールです。これは、ブラウザが ネイティブでサポートしています

CSP(コンテンツ セキュリティ ポリシー)と Polymer

セキュアなウェブ アプリケーションを開発するには、CSP を実装する必要があります。 CSP は、クロスサイト スクリプティング(XSS)攻撃(安全でないソースからのスクリプトの実行、HTML ファイルからのインライン スクリプトの実行)を防ぐ一連のルールです。

これで、最適化、連結、圧縮された .html ファイルが生成されます。 すべての JavaScript コードがインライン構成で 使用できます。これに対処するために クリスパー

Crisper は、HTML ファイルからインライン スクリプトを分割し、 外部 JavaScript ファイルを生成する必要があります。加熱した Crisper で HTML ファイルを作成し、elements.htmlelements.jselements.html 内では、 収益: elements.js

アプリケーションの論理構造

Polymer では、要素は、非視覚的なユーティリティから、小さなスタンドアロンの再利用可能な UI 要素(ボタンなど)、ページなどの大きなモジュール、さらには完全なアプリケーションの作成まで、あらゆるものになります。

アプリケーションの最上位の論理構造 <ph type="x-smartling-placeholder">
</ph> 以下で表されるアプリケーションの最上位の論理構造 ポリマー要素。

ポリマーと親子アーキテクチャを使用した後処理

どの 3D グラフィック パイプラインでも、必ず最後のステップで 一種のオーバーレイとして画像全体の上に追加されます。これが 後処理ステップがあり、グロー、神の光、光線などの効果が 被写界深度、ボケ、ぼかしなどです シーンの構築方法に応じて異なる要素が生成されます。THREE.js では、JavaScript でポスト処理用のカスタム シェーダーを作成できます。また、親子構造のおかげで、Polymer でも作成できます。

ポスト プロセッサの要素の HTML コードを見ると、次のようになります。

<dom-module id="sw-experience-postprocessor">
    <!-- Template-->
    <template>
    <sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
    <sw-experience-effect-dof class="effect"></sw-experience-effect-dof>
    <sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>

効果は、共通のクラスの下にネストされた Polymer 要素として指定します。次に、 sw-experience-postprocessor.js では、次のようにします。

effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects

HTML 機能と JavaScript の querySelectorAll を使用して、 HTML 要素としてネストしたエフェクトを、次の順序で 指定されています。これらを反復処理して Composer に追加します。

次に、DOF(被写界深度)エフェクトを削除し、ブローエフェクトとビネットエフェクトの順序を変更したいとします。あとは編集するだけで ポスト プロセッサの定義は次のようになります。

<dom-module id="sw-experience-postprocessor">
    <!-- Template-->
    <template>
    <sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
    <sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>

実際のコードは 1 行も変更せずに、シーンが実行されます。

Polymer のレンダリング ループと更新ループ

Polymer を使用すると、レンダリングとエンジンの更新もエレガントに行うことができます。requestAnimationFrame を使用して計算する timer 要素を作成しました。 値(現在の時刻(t)や 最後のフレーム(dt):

Polymer
    is: 'sw-timer'

    properties:
    t:
        type: Number
        value: 0
        readOnly: true
        notify: true
    dt:
        type: Number
        value: 0
        readOnly: true
        notify: true

    _isRunning: false
    _lastFrameTime: 0

    ready: ->
    @_isRunning = true
    @_update()

    _update: ->
    if !@_isRunning then return
    requestAnimationFrame => @_update()
    currentTime = @_getCurrentTime()
    @_setT currentTime
    @_setDt currentTime - @_lastFrameTime
    @_lastFrameTime = @_getCurrentTime()

    _getCurrentTime: ->
    if window.performance then performance.now() else new Date().getTime()

次に、データ バインディングを使用して、t プロパティと dt プロパティをエンジン(experience.jade)にバインドします。

sw-timer(
    t='{ % templatetag openvariable % }t}}',
    dt='{ % templatetag openvariable % }dt}}'
)

sw-experience-engine(
    t='[t]',
    dt='[dt]'
)

エンジンで tdt の変更をリッスンし、値が変更されるたびに _update 関数が呼び出されます。

Polymer
    is: 'sw-experience-engine'

    properties:
    t:
        type: Number

    dt:
        type: Number

    observers: [
    '_update(t)'
    ]

    _update: (t) ->
    dt = @dt
    @_physics.update dt, t
    @_renderer.render dt, t

FPS が必要な場合は、Polymer のデータを削除することをおすすめします。 レンダリング ループでのバインディングにより、通知に必要なミリ秒数を削減 変更に関する要素を作成します。カスタム オブザーバーは次のように実装しました。

sw-timer.coffee:

addUpdateListener: (listener) ->
    if @_updateListeners.indexOf(listener) == -1
    @_updateListeners.push listener
    return

removeUpdateListener: (listener) ->
    index = @_updateListeners.indexOf listener
    if index != -1
    @_updateListeners.splice index, 1
    return

_update: ->
    # ...
    for listener in @_updateListeners
        listener @dt, @t
    # ...

addUpdateListener 関数はコールバックを受け取り、 配列。その後、更新ループですべてのコールバックを反復処理し、 データ バインディングやデータ バインディングをバイパスして、dt 引数と t 引数を直接指定して実行します。 発生します。コールバックが本来有効ではなくなったら、 removeUpdateListener 関数を使用すると、以前に追加したコールバックを削除できます。

THREE.js でのライトセーバー

THREE.js は WebGL の細部を抽象化して、 焦点を当てています問題はストームトルーパーとの戦いです あります。では、ライトセーバーを作成しましょう。

光るブレードは古いライトセーバーと区別できる あります。主に梁と歩道の 2 つの部分で構成されています。 可動式に動かしただけです。明るい円柱の形状で、プレーヤーの動きに合わせて追従する動的なトレイルを備えています。

ブレード

羽根は 2 つの副羽根で構成されています。内側と外側の 2 種類です。 どちらも素材を使用した THREE.js メッシュです。

内刃

内側のブレードには、カスタム マテリアルとカスタム シェーダーを使用しました。2 つのポイントで作成された線を取り、これらの 2 つのポイント間の線を平面に投影します。このプレーンは基本的に、モバイルで戦闘するときに操作するもので、セイバーに奥行きと向きを感知させます。

丸く光る物体の感覚を作るには、 メインから平面上の任意の点の直交点距離 以下のように、A と B の 2 つの点を結ぶ線を描きます。拡張ポイントが 主軸が明るくなるほどです。

ブレードの内側の光

次のソースは、vFactor を計算して頂点シェーダーの輝度を制御し、それをフラグメント シェーダーでシーンとブレンドする方法を示しています。

THREE.LaserShader = {

    uniforms: {
    "uPointA": {type: "v3", value: new THREE.Vector3(0, -1, 0)},
    "uPointB": {type: "v3", value: new THREE.Vector3(0, 1, 0)},
    "uColor": {type: "c", value: new THREE.Color(1, 0, 0)},
    "uMultiplier": {type: "f", value: 3.0},
    "uCoreColor": {type: "c", value: new THREE.Color(1, 1, 1)},
    "uCoreOpacity": {type: "f", value: 0.8},
    "uLowerBound": {type: "f", value: 0.4},
    "uUpperBound": {type: "f", value: 0.8},
    "uTransitionPower": {type: "f", value: 2},
    "uNearPlaneValue": {type: "f", value: -0.01}
    },

    vertexShader: [

    "uniform vec3 uPointA;",
    "uniform vec3 uPointB;",
    "uniform float uMultiplier;",
    "uniform float uNearPlaneValue;",
    "varying float vFactor;",

    "float getDistanceFromAB(vec2 a, vec2 b, vec2 p) {",

        "vec2 l = b - a;",
        "float l2 = dot( l, l );",
        "float t = dot( p - a, l ) / l2;",
        "if( t < 0.0 ) return distance( p, a );",
        "if( t > 1.0 ) return distance( p, b );",
        "vec2 projection = a + (l * t);",
        "return distance( p, projection );",

    "}",

    "vec3 getIntersection(vec4 a, vec4 b) {",

        "vec3 p = a.xyz;",
        "vec3 q = b.xyz;",
        "vec3 v = normalize( q - p );",
        "float t = ( uNearPlaneValue - p.z ) / v.z;",
        "return p + (v * t);",

    "}",

    "void main() {",

        "vec4 a = modelViewMatrix * vec4(uPointA, 1.0);",
        "vec4 b = modelViewMatrix * vec4(uPointB, 1.0);",
        "if(a.z > uNearPlaneValue) a.xyz = getIntersection(a, b);",
        "if(b.z > uNearPlaneValue) b.xyz = getIntersection(a, b);",
        "a = projectionMatrix * a; a /= a.w;",
        "b = projectionMatrix * b; b /= b.w;",
        "vec4 p = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
        "gl_Position = p;",
        "p /= p.w;",
        "float d = getDistanceFromAB(a.xy, b.xy, p.xy) * gl_Position.z;",
        "vFactor = 1.0 - clamp(uMultiplier * d, 0.0, 1.0);",

    "}"

    ].join( "\n" ),

    fragmentShader: [

    "uniform vec3 uColor;",
    "uniform vec3 uCoreColor;",
    "uniform float uCoreOpacity;",
    "uniform float uLowerBound;",
    "uniform float uUpperBound;",
    "uniform float uTransitionPower;",
    "varying float vFactor;",

    "void main() {",

        "vec4 col = vec4(uColor, vFactor);",
        "float factor = smoothstep(uLowerBound, uUpperBound, vFactor);",
        "factor = pow(factor, uTransitionPower);",
        "vec4 coreCol = vec4(uCoreColor, uCoreOpacity);",
        "vec4 finalCol = mix(col, coreCol, factor);",
        "gl_FragColor = finalCol;",

    "}"

    ].join( "\n" )

};

アウターブレードグロウ

外側のグローについては別のレンダーバッファにレンダリングし、 最終的な画像にブレンドして 調整します。以下の図は、Google Cloud で提供する 3 つの異なるリージョン セイバーが手に入ったら手に入ります。すなわち、真ん中の白いコア、 青がかった光と外側の光です。

外側のブレード

ライトセーバー トレイル

ライトセーバーの足跡が、原作と同じ効果の鍵を握る スター・ウォーズシリーズの三角形のファンを使ってトレイルを作りました。 ライトセーバーの動きに応じて動的に変化しますそのファンは 視覚効果をさらに高めるためにポストプロセッサに 渡されます作成方法: 直線セグメントがあり、その前の変換に基づく扇形のジオメトリ 現在の変換により、メッシュに新しい三角形が生成されます。 一定の長さ後にテール部分を取り除くことができます

ライトセーバー トレイル(左)
ライトセーバー トレイル(右)

メッシュを作成したら、単純なマテリアルをそのメッシュに割り当てて、 滑らかな効果を作り出します。Google 検索と同じブルーム効果を 外側のブレードに光を当てて、滑らかな軌跡を描くようにしました。

完全なトレイル

トレイルで輝く

最後のピースを完成させるためには、実際の周囲のグローに いくつかの方法で作成できます。パフォーマンス上の理由から、Google のソリューションは、このバッファ用にカスタム シェーダーを作成し、レンダラバッファのクリップの周囲に滑らかなエッジを作成することです。ここでは詳しく説明しません。その後、この出力を最終的なレンダリングで組み合わせます。これにより、 トレイルを輝かせましょう:

キラキラ光るトレイル

まとめ

Polymer は強力なライブラリでありコンセプトです(WebComponents の 全般)。それを使って何を作るかはあなた次第です。任意の範囲から シンプルな UI ボタンからフルサイズの WebGL アプリケーションにアクセスできます。これまでのチャプターで Polymer を効率的に使用するためのヒントとコツを 実行も実行できるより複雑なモジュールの構成方法について ありますまた、WebGL で見栄えの良いライトセーバーを作成する方法もご紹介しました。 これらをすべて組み合わせる場合は、本番環境サーバーにデプロイする前に Polymer 要素を Vulcanize し、CSP に準拠した状態を維持したい場合は Crisper を使用します。

ゲームプレイ