摘要
我們如何使用 Polymer 來建立可模組化及設定的高效能 WebGL 行動控制 Lightsaber。我們整理了專案 https://lightsaber.withgoogle.com/ 的部分重要詳細資料,可協助您在下次遇到種種憤怒的 Stormtroopers 時節省寶貴時間。
總覽
如果您想知道什麼是 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 檔案) 和範本 (jade 檔案) 分別建立目錄和檔案。
以下是 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
檔案。這可告知 Boyer 將依附元件的儲存位置,以便我們確保同一個目錄中的結尾處只有一個:
{
"directory" : "../../../../../bower_components"
}
這樣一來,如果多個元素宣告 THREE.js
為依附元件,當 Boyer 安裝第一個元素並開始剖析第二個元素後,就會知道這個依附元件已安裝,不會重新下載或複製。同樣地,只要 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 所需的元素, 必須執行下列步驟:
- 將元素的
.coffee
檔案編譯為.js
- 將元素的
.scss
檔案編譯為.css
- 將元素的
.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 版本,我們最終會將 Polymer 專案用於實際工作環境。
Vulcanize 是一個 Polymer 工具,可將依附元件樹狀結構壓縮成單一 HTML 檔案,進而減少要求數量。特別適合未原生支援網頁元件的瀏覽器。
CSP (內容安全政策) 和 Polymer
開發安全的網頁應用程式時,您必須實作 CSP。CSP 是一組可防止跨網站指令碼 (XSS) 攻擊的規則:從不安全的來源執行指令碼,或從 HTML 檔案執行內嵌指令碼。
現在,Vulcanize 產生的 .html
檔案經過最佳化、串連和壓縮,所有 JavaScript 程式碼都是內嵌以不符合 CSP 格式的格式。為此,我們使用名為 Crisper 的工具。
Crisper 會將內嵌指令碼從 HTML 檔案分割成一個外部 JavaScript 檔案,以便遵守 CSP 規定。因此,我們會透過 Crisper 傳送隱形的 HTML 檔案,最後產生兩個檔案:elements.html
和 elements.js
。在 elements.html
中,系統也會載入產生的 elements.js
。
應用程式邏輯結構
在 Polymer 中,元素可以是非視覺公用程式,也可以是可重複使用的小型 UI 元素 (例如按鈕),到「頁面」等較大模組,甚至可組合完整應用程式。
使用 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 元素巢狀結構的所有效果。然後反覆疊代,並將其新增至作曲家。
現在,假設我們要移除「控制深度」(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()
接著,我們使用資料繫結將 t
和 dt
屬性繫結至引擎 (experience.jade
):
sw-timer(
t='{ % templatetag openvariable % }t}}',
dt='{ % templatetag openvariable % }dt}}'
)
sw-experience-engine(
t='[t]',
dt='[dt]'
)
我們會監聽引擎中 t
和 dt
的變更,且只要值有所改變,就會呼叫 _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 的低階細節,讓我們能專心解決問題。我們目前是對抗暴風雨者 我們需要武器我們來打造光劍吧
閃光刀片是光劍與任何舊雙手武器的區別。主要由兩個部分組成:橫束和移動時看到的步道。我們使用亮圓柱形狀來建構這個應用程式 加上隨玩家移動的動態軌跡
刀鋒
刀片是由兩個子刀片組成。內部和外型。兩者的材質皆是 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" )
};
外部刀片光暈
針對外光,我們會轉譯至獨立的轉譯緩衝區,並使用後處理繁花效果,並與最終圖片混合,獲得想要的光暈。下圖顯示了想要購買香蕉的三個不同區域。也就是白色核心、中藍的光暈和外部光暈。
光劍步道
光劍的步道是《星際大戰》系列中原始影片的完整效果,我們根據光劍的移動情形,動態產生了三角形的粉絲群接著,這些風扇會傳送至後置處理器進行進一步的視覺強化。為了建立粉絲幾何圖形,我們會依據其先前的轉換和目前的轉換,設定一條線段,並在一定長度之後捨棄尾端部分。
掌握了網格後,我們就會為其指派簡易材料,並傳遞至郵遞處理器,以創造流暢效果。我們使用與外葉光暈相同的花卉效果,並取得順暢的步道,如下所示:
步道周圍的光暈
最後一個部分要完成,我們得在實際軌道周圍處理光暈,以多種方式建立。但基於效能考量,我們的解決方案是為這個緩衝區建立自訂著色器,在轉譯緩衝區的取值周圍形成平滑的邊緣。然後,我們會在最終的轉譯結果中合併這些輸出內容,這樣就能看到步道圍繞著的光暈:
結語
Polymer 是功能強大的程式庫和概念 (一般與 WebComponents 相同)。完全由您決定。從簡單的 UI 按鈕,到原尺寸的 WebGL 應用程式都屬於這個類別。在先前的章節中,我們展示了一些提示與秘訣,協助您瞭解如何在實際工作環境中使用 Polymer,以及如何建構較複雜的模組,而且效能良好。此外,我們也示範如何透過 WebGL 呈現美觀的光纖。 因此,如果結合所有事項,在部署至實際工作環境伺服器之前,請記得先 Vulcanize Polymer 元素。假如您想保持使用 Crisper 來遵守 CSP 規範,這可能對您造成的主力!