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

Ảnh chụp màn hình của Lightaber

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:

  1. Biên dịch các phần tử .coffee tệp đến .js
  2. Biên dịch các phần tử .scss tệp đến .css
  3. 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.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.htmlelements.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 đủ.

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 biểu thị bằng các phần tử Polymer.

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 tdt 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 tdt 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ố dtt, 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.

Lớp sáng lưỡi dao bên trong

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.

Lưỡi 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.

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

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àn bộ đường mòn

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:

Đường mòn có ánh sáng

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!

Cảnh chơi trò chơi