Tóm tắt
Cách chúng tôi sử dụng Polymer để tạo ra một WebGL hiệu suất cao được kiểm soát dành cho thiết bị di động Lightaber theo mô-đun và có thể định cấu hình. Chúng tôi sẽ xem xét một số thông tin quan trọng thuộc dự án https://lightsaber.withgoogle.com/ của chúng tôi để giúp bạn tiết kiệm thời gian khi tự tạo đội quân bão giận dữ.
Tổng quan
Nếu bạn đang thắc mắc về các thành phần Polymer hoặc WebComponents, tốt nhất là bắt đầu bằng cách chia sẻ bản trích xuất từ một dự án thực tế đang làm việc. Dưới đây là một mẫu lấy từ trang đích của dự án https://lightsaber.withgoogle.com. Đây là một tệp HTML thông thường nhưng có một số tính năng thú vị bên trong:
<!-- 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>
Vì vậy, hiện nay, bạn có nhiều lựa chọn khi muốn tạo một ứng dụng dựa trên HTML5. API, Khung, Thư viện, Công cụ phát triển trò chơi, v.v. Mặc dù có tất cả các lựa chọn, nhưng rất khó để có được một thiết lập kết hợp tốt giữa việc kiểm soát hiệu suất cao của đồ hoạ và các mô-đun sạch cấu trúc và khả năng có thể mở rộng. Chúng tôi nhận thấy rằng Polymer có thể giúp chúng tôi sắp xếp dự án một cách hợp lý mà vẫn cho phép tối ưu hoá hiệu suất cấp thấp. Chúng tôi đã cẩn thận thiết kế cách chia dự án thành các thành phần để tận dụng tối đa các tính năng của Polymer.
Mô-đun kết hợp với polymer
Polymer là một thư viện cho phép rất nhiều sức mạnh đối với cách dự án của bạn được xây dựng từ các phần tử tuỳ chỉnh có thể tái sử dụng. API này cho phép bạn sử dụng các mô-đun độc lập, có đầy đủ chức năng có trong một tệp HTML đơn lẻ. Chúng không chỉ chứa cấu trúc (đánh dấu HTML) mà còn chứa kiểu và logic cùng dòng.
Hãy xem ví dụ dưới đây:
<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>
Nhưng trong một dự án lớn hơn, việc tách riêng ba thành phần (HTML, CSS, JS) và chỉ hợp nhất chúng tại thời điểm biên dịch. Vì vậy, một việc chúng ta đã làm là cung cấp cho mỗi phần tử trong dự án một thư mục riêng:
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
Và thư mục của mỗi phần tử có cùng cấu trúc nội bộ với các thư mục và tệp riêng biệt cho logic (tệp coffee), kiểu (tệp scss) và mẫu (tệp jade).
Dưới đây là một phần tử sw-ui-logo
mẫu:
sw-ui-logo/
|-- bower.json
|-- scripts
| `-- sw-ui-logo.coffee
|-- styles
| `-- sw-ui-logo.scss
`-- sw-ui-logo.jade
Và nếu bạn xem xét tệp .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')
Bạn có thể thấy cách sắp xếp mọi thứ một cách gọn gàng bằng cách đưa các kiểu và logic từ các tệp riêng biệt vào. Để đưa các kiểu dáng của chúng tôi vào Polymer
các phần tử chúng tôi sử dụng câu lệnh include
của Jade, vì vậy chúng tôi có CSS cùng dòng thực tế
nội dung tệp sau khi biên dịch. Phần tử tập lệnh sw-ui-logo.js
sẽ
thực thi trong thời gian chạy.
Phần phụ thuộc theo mô-đun với Bower
Thông thường, chúng ta giữ thư viện và các phần phụ thuộc khác ở cấp dự án.
Tuy nhiên, trong quá trình thiết lập ở trên, bạn sẽ thấy một bower.json
trong
thư mục của phần tử: các phần phụ thuộc cấp phần tử. Ý tưởng đằng sau phương pháp này
trong trường hợp bạn có nhiều yếu tố với các kiểu
Chúng tôi có thể đảm bảo chỉ tải những phần phụ thuộc đó
thực sự được sử dụng. Nếu xoá một phần tử, bạn không cần phải nhớ
xoá phần phụ thuộc vì bạn cũng sẽ xoá tệp bower.json
để khai báo các phần phụ thuộc này. Mỗi phần tử đều tải độc lập
các phần phụ thuộc có liên quan đến nó.
Tuy nhiên, để tránh trùng lặp các phần phụ thuộc, chúng ta sẽ bao gồm tệp .bowerrc
trong thư mục của từng phần tử. Thông tin này cho cung cấp thông tin về nơi lưu trữ
để có thể đảm bảo chỉ có một phần phụ thuộc ở cuối trong cùng một phần
thư mục:
{
"directory" : "../../../../../bower_components"
}
Bằng cách này, nếu nhiều phần tử khai báo THREE.js
làm phần phụ thuộc, sau khi bower cài đặt phần phụ thuộc này cho phần tử đầu tiên và bắt đầu phân tích cú pháp phần tử thứ hai, bower sẽ nhận ra rằng phần phụ thuộc này đã được cài đặt và sẽ không tải xuống lại hoặc sao chép phần phụ thuộc này. Tương tự, tệp này sẽ giữ lại các tệp phần phụ thuộc đó miễn là có ít nhất một phần tử vẫn xác định tệp đó trong bower.json
.
Tập lệnh bash tìm tất cả tệp bower.json
trong cấu trúc các phần tử lồng nhau.
Sau đó, thao tác này sẽ nhập từng thư mục này rồi thực thi bower install
trong
từng yếu tố trong số đó:
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
Mẫu phần tử mới nhanh
Mỗi lần bạn muốn tạo phần tử mới sẽ mất một chút thời gian: tạo thư mục và cấu trúc tệp cơ bản với tên chính xác. Vì vậy, chúng tôi sử dụng Slush để viết một trình tạo phần tử đơn giản.
Bạn có thể gọi tập lệnh từ dòng lệnh:
$ slush element path/to/your/element-name
Và phần tử mới sẽ được tạo, bao gồm toàn bộ cấu trúc và nội dung tệp.
Chúng ta đã xác định các mẫu cho các tệp phần tử, ví dụ: mẫu tệp .jade
có dạng như sau:
// 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')
Trình tạo Slush thay thế các biến bằng tên và đường dẫn phần tử thực tế.
Sử dụng Gulp để tạo các phần tử
Gulp giúp quy trình xây dựng nằm trong tầm kiểm soát. Trong cấu trúc của chúng tôi, để xây dựng các phần tử chúng ta cần Gulp theo các bước sau:
- Biên dịch các phần tử
.coffee
tệp đến.js
- Biên dịch các phần tử
.scss
tệp đến.css
- Biên dịch các phần tử
.jade
tệp vào.html
, nhúng các tệp.css
.
Chi tiết hơn:
Biên dịch tệp .coffee
của các phần tử thành .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')));
});
Đối với các bước 2 và 3, chúng ta sử dụng gulp và trình bổ trợ compass để biên dịch scss
thành .css
và .jade
thành .html
, theo phương pháp tương tự như bước 2 ở trên.
Bao gồm các phần tử Polymer
Để thực sự đưa vào các phần tử Polymer, chúng ta sử dụng tính năng nhập 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">
Tối ưu hoá các phần tử Polymer cho sản xuất
Một dự án lớn có thể có nhiều phần tử Polymer. Trong dự án của mình, chúng tôi có hơn 50 lớp. Nếu bạn xem xét mỗi phần tử có một tệp .js
riêng biệt và một số phần tử có thư viện được tham chiếu, thì sẽ có hơn 100 tệp riêng biệt. Điều này có nghĩa là trình duyệt cần thực hiện rất nhiều yêu cầu,
do hiệu suất giảm. Tương tự như quá trình nối và rút gọn, chúng ta
sẽ áp dụng cho một bản dựng Angular, chúng tôi đã “lưu hoá” dự án Polymer ở
kết thúc sản xuất.
Vulcanize là một công cụ Polymer làm phẳng cây phụ thuộc thành một tệp html duy nhất, giảm số lượng yêu cầu. Điều này đặc biệt tuyệt vời đối với các trình duyệt không hỗ trợ sẵn các thành phần web.
CSP (Chính sách bảo mật nội dung) và Polymer
Khi phát triển các ứng dụng web bảo mật, bạn cần triển khai CSP. CSP là một bộ quy tắc ngăn chặn các cuộc tấn công tập lệnh trên nhiều trang web (XSS): thực thi tập lệnh từ các nguồn không an toàn hoặc thực thi tập lệnh nội tuyến từ các tệp HTML.
Bây giờ, tệp .html
được tối ưu hoá, nối và giảm kích thước đã tạo
của Vulcanize có tất cả mã JavaScript cùng dòng trong một tệp không tuân thủ CSP
. Để giải quyết vấn đề này, chúng tôi dùng một công cụ có tên là
Bánh nướng.
Sắc thái phân tách các tập lệnh nội tuyến từ một tệp HTML và đặt chúng thành một
tệp JavaScript bên ngoài để tuân thủ CSP. Vì vậy, chúng tôi chuyển
HTML thông qua Crisper và kết thúc với hai tệp: elements.html
và
elements.js
. Bên trong elements.html
, tính năng này cũng đảm nhận việc tải
đã tạo elements.js
.
Cấu trúc logic của ứng dụng
Trong Polymer, các phần tử có thể là bất kỳ thứ gì, từ một tiện ích không hình ảnh đến các phần tử giao diện người dùng nhỏ, độc lập và có thể sử dụng lại (như nút) cho đến các mô-đun lớn hơn như "trang" và thậm chí là tạo ứng dụng đầy đủ.
Xử lý hậu kỳ bằng Polymer và Kiến trúc mẹ con
Trong bất kỳ quy trình đồ hoạ 3D nào, luôn có một bước cuối cùng là thêm hiệu ứng lên trên toàn bộ hình ảnh dưới dạng một loại lớp phủ. Đây là bước xử lý hậu kỳ và bao gồm các hiệu ứng như phát sáng, tia thần độ sâu trường ảnh, bokeh, làm mờ, v.v. Các hiệu ứng này được kết hợp và áp dụng cho các yếu tố khác nhau theo cách cảnh được xây dựng. Trong THREE.js, chúng tôi có thể tạo chương trình đổ bóng tuỳ chỉnh để xử lý hậu kỳ trong JavaScript hoặc chúng ta có thể thực hiện điều này bằng Polymer, nhờ cấu trúc mẹ con.
Nếu bạn nhìn vào mã HTML phần tử của bộ xử lý bài đăng của chúng tôi:
<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>
Chúng ta chỉ định hiệu ứng là các phần tử Polymer lồng trong một lớp chung. Sau đó:
trong sw-experience-postprocessor.js
, chúng ta sẽ thực hiện việc này:
effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects
Chúng tôi sử dụng tính năng HTML và querySelectorAll
của JavaScript để tìm tất cả
các hiệu ứng được lồng dưới dạng các phần tử HTML trong trình xử lý bài đăng, theo thứ tự
mà chúng được chỉ định. Sau đó, chúng tôi lặp lại và thêm chúng vào trình soạn thảo.
Bây giờ, giả sử chúng ta muốn xoá hiệu ứng DOF (Độ sâu trường) và thay đổi thứ tự hoa và hiệu ứng làm mờ nét ảnh. Tất cả những gì chúng tôi cần làm là chỉnh sửa định nghĩa của bộ xử lý hậu kỳ thành một ví dụ như:
<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>
và cảnh sẽ chạy mà không cần thay đổi một dòng mã thực tế nào.
Kết xuất vòng lặp và cập nhật vòng lặp trong Polymer
Với Polymer, chúng ta cũng có thể tiến hành kết xuất và cập nhật công cụ một cách tinh tế.
Chúng ta đã tạo một phần tử timer
sử dụng requestAnimationFrame
và tính toán
các giá trị như thời gian hiện tại (t
) và thời gian delta - thời gian trôi qua từ
khung hình cuối cùng (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()
Sau đó, chúng ta sử dụng tính năng liên kết dữ liệu để liên kết các thuộc tính t
và dt
với công cụ (experience.jade
):
sw-timer(
t='{ % templatetag openvariable % }t}}',
dt='{ % templatetag openvariable % }dt}}'
)
sw-experience-engine(
t='[t]',
dt='[dt]'
)
Đồng thời, chúng tôi theo dõi các thay đổi của t
và dt
trong công cụ và bất cứ khi nào
giá trị thay đổi, thì hàm _update
sẽ được gọi:
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
Tuy nhiên, nếu muốn có FPS cao, bạn nên xoá liên kết dữ liệu của Polymer trong vòng lặp kết xuất để tiết kiệm vài mili giây cần thiết để thông báo cho các phần tử về các thay đổi. Chúng tôi đã triển khai đối tượng tiếp nhận dữ liệu tuỳ chỉnh như sau:
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
# ...
Hàm addUpdateListener
chấp nhận một lệnh gọi lại và lưu lệnh đó trong
mảng callback. Sau đó, trong vòng lặp cập nhật, chúng tôi lặp lại từng lệnh gọi lại và
chúng ta sẽ thực thi trực tiếp với các đối số dt
và t
, bỏ qua liên kết dữ liệu hoặc
kích hoạt sự kiện. Khi lệnh gọi lại không còn hoạt động nữa, chúng tôi đã thêm một
Hàm removeUpdateListener
cho phép bạn xoá một lệnh gọi lại đã thêm trước đó.
Thanh kiếm ánh sáng trong THREE.js
THREE.js loại bỏ thông tin chi tiết cấp thấp của WebGL và cho phép chúng tôi tập trung về sự cố. Vấn đề của chúng ta là chiến đấu với Stormtroopers và chúng ta cần một vũ khí. Vậy hãy chế tạo một thanh kiếm ánh sáng.
Lưỡi kiếm phát sáng là điểm phân biệt giữa kiếm ánh sáng với bất kỳ thanh kiếm cũ nào vũ khí hai tay. Thanh này chủ yếu được làm từ hai phần: dầm ngang và đường nhỏ được nhìn thấy khi di chuyển nó. Chúng tôi đã tạo nó bằng hình dạng hình trụ sáng và một vệt sáng động theo sau khi người chơi di chuyển.
Lưỡi cạo
Phần lưỡi dao gồm 2 lưỡi phụ. Bên trong và bên ngoài. Cả hai đều là lưới THREE.js với các vật liệu tương ứng.
Lưỡi cạo bên trong
Đối với lưỡi cắt bên trong, chúng tôi sử dụng một chất liệu tuỳ chỉnh với chương trình đổ bóng tuỳ chỉnh. T4 lấy một đường thẳng được tạo bởi hai điểm và chiếu đường thẳng giữa hai điểm này các điểm trên máy bay. Về cơ bản, chiếc máy bay này là những gì bạn điều khiển khi bạn chiến đấu bằng thiết bị di động, nó mang lại cảm giác có chiều sâu và phương hướng vào thanh kiếm.
Để tạo cảm giác về một vật thể tròn phát sáng, chúng ta xem xét khoảng cách điểm vuông góc của bất kỳ điểm nào trên mặt phẳng từ đường chính nối hai điểm A và B như bên dưới. Điểm càng gần trục chính thì càng sáng.
Nguồn dưới đây cho biết cách chúng tôi tính toán vFactor
để kiểm soát cường độ
trong chương trình đổ bóng đỉnh để sau đó sử dụng nó cho phù hợp với cảnh trong
chương trình đổ bóng mảnh.
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" )
};
Toả sáng ngoài lưỡi dao
Đối với hiệu ứng ánh sáng bên ngoài, chúng ta kết xuất vào một vùng đệm kết xuất riêng biệt và sử dụng hiệu ứng bùng sáng sau khi xử lý, sau đó kết hợp với hình ảnh cuối cùng để có được hiệu ứng ánh sáng mong muốn. Hình ảnh dưới đây hiển thị 3 khu vực mà bạn nếu bạn muốn có một thanh kiếm tốt. Cụ thể là lõi màu trắng, phần giữa ánh sáng màu xanh lam và ánh sáng bên ngoài.
Đường mòn ánh sáng
Vệt của thanh kiếm ánh sáng là yếu tố then chốt để tạo ra hiệu ứng đầy đủ như bản gốc trong loạt video Star Wars. Chúng tôi đã tạo vệt sáng bằng một nhóm các tam giác được tạo động dựa trên chuyển động của thanh kiếm ánh sáng. Sau đó, các quạt này được chuyển đến bộ xử lý hậu kỳ để tăng cường hình ảnh hơn nữa. Để tạo hình quạt, chúng tôi có một đoạn đường thẳng và dựa trên sự biến đổi trước đó của nó và biến đổi hiện tại, chúng ta tạo một tam giác mới trong lưới, thả ra khỏi phần đuôi sau một độ dài nhất định.
Sau khi có một lưới, chúng ta gán một vật liệu đơn giản cho lưới đó và chuyển nó đến bộ xử lý hậu kỳ để tạo hiệu ứng mượt mà. Chúng tôi sử dụng hiệu ứng nở hoa tương tự chúng tôi đã áp dụng cho ánh sáng phiến bên ngoài và tạo ra một vệt êm ái như bạn có thể thấy:
Toả sáng quanh đường mòn
Để hoàn thiện phần cuối cùng, chúng ta phải xử lý ánh sáng xung quanh vệt thực tế. Bạn có thể tạo vệt theo một số cách. Giải pháp của chúng tôi mà chúng tôi không đi sâu vào chi tiết ở đây, vì lý do hiệu suất là tạo một chương trình đổ bóng tuỳ chỉnh cho vùng đệm này để tạo cạnh mượt mà xung quanh một kẹp của vùng đệm kết xuất. Sau đó, chúng tôi kết hợp kết quả này trong lần kết xuất cuối cùng. Tại đây, bạn có thể xem ánh sáng xung quanh đường mòn:
Kết luận
Polymer là một thư viện và khái niệm mạnh mẽ (giống như WebComponents trong chung). Điều này tuỳ thuộc vào việc bạn tạo ra sản phẩm đó như thế nào. Có thể là bất cứ nội dung gì từ một nút giao diện người dùng đơn giản dẫn đến ứng dụng WebGL ở kích thước đầy đủ. Trong các chương trước chúng tôi đã cho bạn thấy một số mẹo và thủ thuật về cách sử dụng Polymer hiệu quả trong quá trình sản xuất và cách cấu trúc các mô-đun phức tạp hơn cũng hoạt động tốt. Chúng tôi cũng đã hướng dẫn bạn cách tạo thanh kiếm ánh sáng đẹp mắt trong WebGL. Vì vậy, nếu bạn kết hợp tất cả những thứ đó, hãy nhớ Lưu hoá các phần tử Polymer của bạn trước khi triển khai cho máy chủ sản xuất và nếu bạn không quên sử dụng Crisper nếu muốn luôn tuân thủ CSP, thì bạn có thể áp dụng biện pháp này!