Tạo thanh kiếm ánh sáng bằng Polymer

Ảnh chụp màn hình tối đa của người xưa

Tóm tắt

Cách chúng tôi sử dụng Polymer để tạo đèn chiếu sáng được điều khiển bằng thiết bị di động WebGL hiệu suất cao, có thể được điều khiển theo mô-đun và có thể định cấu hình. Chúng tôi xem xét một số thông tin chi tiết chính của dự án https://lightsaber.withgoogle.com/ để giúp bạn tiết kiệm thời gian khi tự sáng tạo nội dung trong lần tiếp theo gặp phải một bầy Stormtroopers đang tức giận.

Tổng quan

Nếu bạn đang thắc mắc về Polymer hoặc WebComponents nào, thì tốt nhất là bạn nên chia sẻ bản trích xuất của một dự án thực tế đang hoạt động. Dưới đây là 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ố điều kỳ diệu 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 có nhiều lựa chọn khi bạn muốn tạo ứ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ả lựa chọn, nhưng rất khó để có được một thiết lập kết hợp hài hoà giữa khả năng kiểm soát hiệu suất cao của đồ hoạ với cấu trúc mô-đun và khả năng mở rộng sạch sẽ. 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 khoa học trong khi 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 xây dựng cách chia nhỏ 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 hoá bằng polymer

Polymer là một thư viện cho phép bạn thay đổi cách xây dựng dự án từ các phần tử tuỳ chỉnh có thể sử dụng lại. Giải pháp 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 duy nhất. Các mã này không chỉ chứa cấu trúc (mã đánh dấu HTML) mà còn chứa cả logic và kiểu 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 đối với một dự án lớn hơn, bạn nên phân tách 3 thành phần logic này (HTML, CSS, JS) và chỉ hợp nhất chúng tại thời điểm biên dịch. Chúng tôi đã cung cấp cho từng 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 cà phê), kiểu (tệp scss) và mẫu (tệp ngọc bích).

Dưới đây là ví dụ về phần tử sw-ui-logo:

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

Và nếu bạn nhìn vào 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ể sắp xếp gọn gàng mọi thứ bằng cách thêm kiểu và logic từ các tệp riêng biệt. Để đưa kiểu của mình vào các phần tử Polymer, chúng tôi sử dụng câu lệnh include của Clara, do đó chúng tôi có nội dung tệp CSS cùng dòng thực tế 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 mô-đun với Bower

Thông thường, chúng tôi lưu giữ các thư viện và phần phụ thuộc khác ở cấp dự án. Tuy nhiên, trong cách thiết lập ở trên, bạn sẽ thấy một bower.json nằm 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 là trong trường hợp bạn có nhiều phần tử có nhiều phần phụ thuộc khác nhau, chúng ta có thể đảm bảo chỉ tải các phần phụ thuộc thực sự được sử dụng. Và nếu xoá một phần tử, bạn không cần nhớ xoá phần phụ thuộc của phần tử đó 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ử sẽ tải độc lập các phần phụ thuộc liên quan đến phần tử đó.

Tuy nhiên, để tránh trùng lặp các phần phụ thuộc, chúng tôi cũng đưa một tệp .bowerrc vào thư mục của mỗi phần tử. Điều này sẽ cho bower biết vị trí lưu trữ các phần phụ thuộc để chúng ta có thể đảm bảo chỉ có một phần phụ thuộc ở cuối cùng một thư mục:

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

Theo cách này, nếu nhiều phần tử khai báo THREE.js là phần phụ thuộc, thì sau khi bower cài đặt phần tử 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, bạn 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 đó. Tương tự như vậy, trình phân tích cú pháp 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 của nó.

Tập lệnh bash sẽ tìm tất cả các tệp bower.json trong cấu trúc phần tử lồng nhau. Sau đó, trình phân tích cú pháp sẽ nhập từng thư mục này và thực thi bower install trong mỗi thư mục:

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

Sẽ mất một chút thời gian mỗi khi bạn muốn tạo một phần tử mới: 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 được tạo, bao gồm tất cả cấu trúc và nội dung tệp.

Chúng tôi đã xác định các mẫu cho các tệp phần tử, ví dụ: mẫu tệp .jade sẽ 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 thực tế của phần tử.

Sử dụng Gulp để tạo phần tử

Gulp giúp quy trình xây dựng trong tầm kiểm soát. Và trong cấu trúc của chúng ta, để tạo các phần tử, chúng ta cần Gulp thực hiện theo các bước sau:

  1. Biên dịch các tệp .coffee của các phần tử thành .js
  2. Biên dịch các tệp .scss của các phần tử thành .css
  3. Biên dịch các tệp .jade của các phần tử thành .html, nhúng các tệp .css.

Thông tin chi tiết hơn:

Biên dịch các 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 bước 2 và 3, chúng tôi sử dụng gulp và trình bổ trợ la bàn để biên dịch scss thành .css.jade thành .html, theo phương pháp tương tự như 2 ở trên.

Bao gồm các thành phần polymer

Để thực sự thêm 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 thành phần polymer để sản xuất

Một dự án lớn có thể sẽ có nhiều thành phần polymer. Trong dự án của mình, chúng tôi có hơn 50. Nếu bạn coi mỗi phần tử có một tệp .js riêng và một số phần tử có thư viện được tham chiếu, thì các phần tử đó sẽ trở thành hơn 100 tệp riêng biệt. Điều này đồng nghĩa với việc trình duyệt cần thực hiện rất nhiều yêu cầu, kèm theo hiệu suất bị giảm. Tương tự như quy trình nối và giảm kích thước mà chúng tôi sẽ áp dụng cho bản dựng Angular, chúng tôi "lưu hoá" dự án Polymer ở cuối quá trình sản xuất.

Vulcanize là một công cụ Polymer giúp làm phẳng cây phần phụ thuộc thành một tệp html duy nhất, giúp 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 cùng dòng từ các tệp HTML.

Tệp .html được tối ưu hoá, nối và rút gọn do Vulcanize tạo ra sẽ có tất cả mã JavaScript cùng dòng ở định dạng không tuân thủ CSP. Để giải quyết vấn đề này, chúng tôi sử dụng một công cụ có tên Crisper.

Crisper chia các tập lệnh cùng dòng từ một tệp HTML và đặt các tập lệnh đó vào một tệp JavaScript bên ngoài duy nhất để tuân thủ CSP. Vì vậy, chúng ta truyền tệp HTML đã lưu hoá thông qua Crisper và kết thúc bằng hai tệp: elements.htmlelements.js. Bên trong elements.html, lớp này cũng đảm nhận việc tải elements.js đã tạo.

Cấu trúc logic ứng dụng

Trong Polymer, các thành phần có thể là bất kỳ thứ gì, từ một tiện ích không trực quan đến các thành phần 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 thành các ứng dụng đầy đủ.

Cấu trúc logic cấp cao nhất của ứng dụng
Cấu trúc logic cấp cao nhất của ứng dụng được thể hiện bằng các phần tử polymer.

Hậu xử lý bằng kiến trúc polymer và cấu trúc mẹ con

Trong bất kỳ quy trình đồ hoạ 3D nào, luôn có bước cuối cùng, trong đó hiệu ứng được thêm vào 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à liên quan đến các hiệu ứng như ánh sáng, tia sáng thần, độ sâu của trường, bokeh, làm mờ, v.v. Hiệu ứng được kết hợp và áp dụng cho các phần tử khác nhau tuỳ theo cách dựng cảnh. Trong BA.js, chúng ta có thể tạo một 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 việc 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 trình xử lý hậu kỳ:

<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 các hiệu ứng dưới dạng các phần tử Polymer lồng nhau 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ả hiệu ứng được lồng dưới dạng phần tử HTML trong trình xử lý bài đăng, theo thứ tự được chỉ định. Sau đó, chúng ta lặp lại rồi thêm chúng vào thành phần kết hợp.

Bây giờ, giả sử chúng ta muốn xoá hiệu ứng DOF (Độ sâu trường ảnh) và thay đổi thứ tự của hiệu ứng hiện lên và làm mờ nét ảnh. Tất cả những gì chúng ta cần làm là chỉnh sửa định nghĩa của trình xử lý hậu kỳ thành một số 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ỉ chạy mà không 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ếp cận kết xuất và cập nhật công cụ một cách tinh tế. Chúng tôi đã 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 tdt với công cụ của chúng ta (experience.jade):

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

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

Đồng thời, chúng ta theo dõi các thay đổi của tdt trong công cụ, và bất cứ khi nào các giá trị thay đổi, 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 sử dụng FPS, 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 nhằm thông báo cho các phần tử về những thay đổi này. Chúng tôi đã triển khai các đố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 đó vào mảng lệnh gọi lại. Sau đó, trong vòng lặp cập nhật, chúng ta lặp lại mọi lệnh gọi lại và thực thi trực tiếp bằng các đối số dtt, bỏ qua việc liên kết dữ liệu hoặc kích hoạt sự kiện. Khi một lệnh gọi lại không còn hoạt động, chúng tôi đã thêm hàm removeUpdateListener cho phép bạn xoá một lệnh gọi lại đã thêm trước đó.

Một thanh kiếm ánh sáng trong BA.js

BA.js tóm tắt chi tiết cấp thấp của WebGL và cho phép chúng ta tập trung vào sự cố. Và vấn đề của chúng ta là chiến đấu với Stormtroopers và chúng ta cần một vũ khí. Hãy cùng tạo một thanh kiếm ánh sáng.

Lưỡi dao phát sáng là điểm khác biệt giữa kiếm ánh sáng với bất kỳ vũ khí hai tay cũ nào. Thanh này chủ yếu được tạo thành từ 2 phần: dầm và đường mòn mà bạn nhìn thấy khi di chuyển. Chúng tôi xây dựng trò chơi này với hình dạng hình trụ sáng và một đường mòn linh động chạy theo khi người chơi di chuyển.

Lưỡi dao

Lưỡi dao gồm 2 lưỡi phụ. Tình trạng bên trong và bên ngoài. Cả hai đều là lưới BA.js với các vật liệu tương ứng.

Lưỡi dao bên trong

Đối với lưỡi dao bên trong, chúng tôi đã sử dụng chất liệu tuỳ chỉnh với chương trình đổ bóng tuỳ chỉnh. Chúng ta 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 trên một mặt phẳng. Về cơ bản, máy bay này là thứ bạn điều khiển khi chiến đấu bằng thiết bị di động. Nó mang lại cảm giác về chiều sâu và hướng cho thanh kiếm.

Để tạo cảm giác về một vật thể phát sáng hình tròn, chúng ta quan sát khoảng cách điểm trực giao của bất kỳ điểm nào trên mặt phẳng tính từ đường chính nối hai điểm A và B như bên dưới. Một điểm càng gần trục chính thì càng sáng.

Lưỡi dao ánh sáng bên trong

Nguồn dưới đây cho thấy 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 hình ảnh này để kết 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" )

};

Tỏa sáng bên ngoài

Đối với ánh sáng bên ngoài, chúng tôi kết xuất tại một vùng đệm kết xuất riêng, đồng thời sử dụng hiệu ứng nở hoa sau xử lý rồi kết hợp với hình ảnh cuối cùng để có được ánh sáng mong muốn. Hình ảnh dưới đây cho thấy ba khu vực khác nhau mà bạn cần có nếu muốn có một thanh kiếm phù hợp. Cụ thể là lõi trắng, ánh sáng màu xanh lam ở giữa và ánh sáng bên ngoài.

Lưỡi dao bên ngoài

Đường mòn Lightaber

Vệt của thanh kiếm ánh sáng là chìa khoá tạo ra hiệu ứng đầy đủ như phần gốc bạn thấy trong loạt phim Star Wars. Chúng tôi tạo đường mòn bằng các quạt hình 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 tới bộ xử lý hậu kỳ để cải thiện thêm về hình ảnh. Để tạo hình học quạt, chúng ta có một đoạn thẳng. Dựa vào phép biến đổi trước đó và biến đổi hiện tại, chúng ta tạo một hình tam giác mới trong lưới, thả phần đuôi ra sau một độ dài nhất định.

Đường mòn Kiếm ánh sáng bên trái
Đường mòn Kiếm ánh sáng bên phải

Sau khi có lưới, chúng ta sẽ gán một vật liệu đơn giản cho lưới đó rồi truyền vật liệu đó cho 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 mà chúng tôi đã áp dụng cho ánh sáng lưỡi dao bên ngoài và có được một vệt nhẵn như bạn có thể thấy:

Toàn bộ đường mòn

Ánh sáng xung quanh đường mòn

Để hoàn thiện tác phẩm cuối cùng, chúng tôi đã phải xử lý ánh sáng xung quanh đường mòn thực tế, có thể được tạo ra theo nhiều cách. Giải pháp của chúng tôi mà chúng tôi không đi sâu vào đâ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 nhẵn xung quanh phần kẹp của bộ đệm kết xuất. Sau đó, chúng ta kết hợp kết quả này trong lượt kết xuất cuối cùng, tại đây bạn có thể thấy ánh sáng xung quanh đường mòn:

Đường mòn với ánh sáng rực rỡ

Kết luận

Polymer là một thư viện và khái niệm rất hiệu quả (tương tự như WebComponents nói chung). Việc bạn tạo ra chỉ là tuỳ thuộc vào bạn. Đó có thể là bất cứ thứ gì, từ một nút giao diện người dùng đơn giản đến một ứng dụng WebGL có kích thước đầy đủ. Trong các chương trước, chúng tôi đã chia sẻ cho bạn một số mẹo và thủ thuật để sử dụng hiệu quả Polymer trong môi trường thực tế và cách sắp xếp cấu trúc của các mô-đun phức tạp hơn mà vẫn hoạt động tốt. Chúng tôi cũng đã hướng dẫn bạn cách để có được một thanh kiếm ánh sáng đẹp trong WebGL. Vì vậy, nếu bạn kết hợp tất cả những yếu tố đó, hãy nhớ Lưu hoá các phần tử Polymer 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 duy trì việc tuân thủ CSP, điều này có thể giúp ích cho bạn!

Cảnh chơi trò chơi