使用 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 實現模組化

Polymer 是一種程式庫, 以可重複使用的自訂元素為基礎,大幅簡化專案開發過程。 可讓您使用單一 HTML 檔案中包含的獨立模組,且功能完整。它們不僅包含結構 (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>

但在大型專案中,最好區分這三項邏輯 這些元件 (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 檔案) 的目錄和檔案 (例如 YAML 檔案)

以下是 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.json 元素的資料夾:元素層級的依附元件我們的做法 就是如果有許多不同的元素 我們 可以確實載入這些依附元件 實際用量移除元素時,就不必記得 移除其依附元件,因為您也會移除 bower.json 檔案 宣告這些依附元件每個元素都會獨立載入 所有相關的依附元件

不過,為了避免依附元件重複,我們納入了 .bowerrc 檔案 請務必檢查每個元素的資料夾中這會告訴 Bower 儲存依附元件的地點,以便確保同一個目錄中只有一個依附元件:

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

這樣一來,如果多個元素將 THREE.js 宣告為依附元件, 開始為第一個元素安裝這個架構,然後開始剖析第二個元素 但會得知依附元件已安裝 才能重新下載或複製同樣地,它會保留該依附元件 但仍有至少一個元素定義該元素 其 bower.json

bash 指令碼可在巢狀元素結構中找到所有 bower.json 檔案。 接著逐一進入這些目錄,在 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 可控管建構程序,在我們架構中 以下為需要 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 元素。在我們 我們有超過五十個專案如果每個元素都具有 獨立的 .js 檔案及部分參照程式庫後,該檔案會大於 100 個不同的檔案。這表示瀏覽器必須發出大量要求 並造成效能損失與「串連和壓縮」程序類似 套用至 Angular 版本時,我們會「升級」位於 Polymer 專案的 Cloud Build 觸發條件

Vulcanize 是一款聚合物工具 將依附元件樹狀結構濃縮成單一 HTML 檔案,減少 以便存取如果瀏覽器不支援這項功能 支援網頁元件

CSP (內容安全政策) 和 Polymer

開發安全的網頁應用程式時,您必須實作 CSP。 CSP 是一套可防止跨網站指令碼攻擊 (XSS) 攻擊的規則: 執行來自不安全來源的指令碼,或執行內嵌指令碼 從 HTML 檔案擷取出來

現在會產生經過最佳化、串連和壓縮的 .html 檔案 透過 Vulcanize,所有 JavaScript 程式碼內嵌於不符合 CSP 規定 格式。為解決這項問題,我們使用 Crisper

Crisper 會從 HTML 檔案分割內嵌指令碼,然後將它們放至單一 適用於 CSP 的外部 JavaScript 檔案。因此,我們會將硫化 HTML 檔案傳送至 Crisper,最後產生兩個檔案:elements.htmlelements.js。在 elements.html 內部,該函式也會負責載入 產生的elements.js

應用程式邏輯結構

在 Polymer 中,元素可以是任何東西,從非視覺化公用程式到小型、獨立且可重複使用的 UI 元素 (例如按鈕),再到「頁面」等大型模組,甚至是組合完整應用程式。

應用程式的頂層邏輯結構
應用程式的頂層邏輯結構,以 Polymer 元素表示。

使用 Polymer 和父項子項架構進行後置處理

在任何 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 元素巢狀形式呈現的效果 都有指定 Pod 的物件然後逐一迴圈並將其加入 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>

場景就會執行,而您不必變更任何程式碼。

在 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()

然後,我們會使用資料繫結將 tdt 屬性繫結至 引擎 (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

但如果您對每秒影格數感到渴望,您可能會想要移除 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 函式會接受回呼,並將其儲存至 回呼陣列。接著,在更新迴圈中,我們會逐一疊代每個回呼,並直接使用 dtt 引數執行回呼,藉此略過資料繫結或事件觸發。當回呼不再處於啟用狀態時,我們新增了 removeUpdateListener 函式,可讓您移除先前新增的回呼。

THREE.js 中的光劍

THREE.js 會抽離 WebGL 的低階細節,讓我們能專注於問題。我們要對抗 Stormtrooper,因此需要武器。我們來建立光劍

光劍與長老的光劍區分 雙手武器它主要由兩個部分組成:光束和移動時可見的軌跡。我們在建造時採用明亮的圓柱形狀 以及隨著玩家移動的動態軌跡

銀河

刀片由兩個子刀片組成。內心和外界。 兩者都是 THREE.js 網格,分別具有各自的材質。

內刀

至於內刀,我們使用了搭配自訂著色器的自訂材質。三 取一個由兩點組成的一條線,然後將兩者的線段劃定 每兩個點就獲得了這個飛機基本上就是 就會形成深度和方向感 我來教你。

為了營造圓形發光物體的效果,我們會查看平面上任一點與連接兩個點 A 和 B 的主線的正交點距離,如下所示。點越接近 主軸的亮度越高

內側刀片

以下來源顯示如何計算 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" )

};

外片光暈

針對外部光暈,我們會轉譯至獨立的轉譯緩衝區,並使用 後製 b 產生 效果 並與最終圖片混合 所需的光暈。下圖顯示您建構的三個不同區域 想要一隻很香的龍捲風就對了名稱為白核心、中空 。

外刀片

Lightsaber Trail

光劍的軌跡是完整影響完整效果的關鍵 在《星際大戰》系列影片中我們利用最後產生的三角形 打造了這條步道 根據光劍的動靜進行動態調整。這些粉絲接著 會傳遞到郵局,進一步增強視覺呈現效果。如要建立 我們設定線段以及依據先前繪製的轉折點 接著會在網格中產生新三角形 達到特定長度後,滑掉尾部分

光劍步道左側
光劍小徑 (右)

找到網格後,我們會為網格指派簡單的材質,並將該網格傳遞至 套用後置處理器可創造流暢的效果我們使用與外部刀片發光效果相同的綻放效果,並取得流暢的軌跡,如下所示:

完整步道

步道周圍的光暈

要完成的最後部分,我們得處理實際周圍的光暈 你可以透過多種方式畫出小道我們的解決方案 其實並沒有詳細說明 但基於效能考量,建立自訂的 這個緩衝區的著色器可在 。接著我們會在最終轉譯時合併這個輸出內容 看看步道周圍的光暈:

發光的軌跡

結論

Polymer 是功能強大的程式庫和概念 (與 WebComponents 一樣, 一般)。至於要如何使用,則由你決定。可以是任何來源 適用於全尺寸 WebGL 應用程式的簡易 UI 按鈕上一章 我們在下方介紹了 Polymer 的 實用提示與秘訣 以及如何建構更複雜的模組,方便執行 我們也示範了如何在 WebGL 中製作出精美的光劍。因此,如果結合這些方法,別忘了對 Polymer 元素 再部署到實際運作伺服器,而且別忘了使用 Crisper 如果希望持續遵守 CSP 法規,你應該成為守護者!

遊戲玩法