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ẫu được lấy từ trang đích của dự án của chúng tôi https://lightsaber.withgoogle.com. Bây giờ 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ả 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 Polymer có thể giúp chúng tôi giữ lại dự án được sắp xếp mà vẫn mang lại hiệu suất thấp và chúng tôi đã tạo dựng cẩn thận cách phân tích dự án vào các thành phần để tận dụng tối đa khả 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. Một điều chúng tôi đã 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 bên trong với các các thư mục và tệp cho logic (tệp cà phê), kiểu (tệp scss) và mẫu (tệp ngọc).

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êm nhiều kiểu để sắp xếp mọi thứ một cách gọn gàng và logic khỏi các tệp riêng biệt. Để đư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 tôi 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à phần phụ thuộc, thì một lần bower cài đặt nó 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 tệp đó. Tương tự, thao tác này sẽ giữ lại 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 các phần tử .coffee tệp đến .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 ta sử dụng gulp và một trình bổ trợ la bàn để biên dịch scss thành .css.jade thành .html, theo cách tương tự như 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, 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 biệt và một số có thư viện được tham chiếu, thì tệp này sẽ lớn 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 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 các tập lệnh cùng dòng từ 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 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 cứ thứ gì từ tiện ích không trực quan cho đến nhỏ, các thành phần độc lập và có thể tái sử dụng trên giao diện người dùng (như nút) thành các mô-đun lớn hơn như "trang" và thậm chí là soạn thảo 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 Nguyên tố polyme.

Xử lý hậu kỳ bằng Kiến trúc polymer và Kiến trúc mẹ con

Trong bất kỳ quy trình đồ hoạ 3D nào, luôn có bước cuối cùng nơi hiệu ứng sẽ được thêm vào bên trên toàn bộ bức ả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ỉ 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ế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ẽ sử dụ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

Nếu khao khát đạt được FPS, bạn nên xoá dữ liệu của Polymer liên kết 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 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à vấn đề của chúng tôi là phải chống lại Lực lượng xung phong và chúng tôi cần có 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 công cụ này bằng một hình trụ sáng và một đường mòn tự động luôn di chuyển 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à chiều hướng vào thanh kiếm.

Để tạo cảm giác như một vật thể phát sáng tròn, chúng ta nhìn vào khoảng cách điểm trực giao của một điểm bất kỳ trên mặt phẳng so với điểm chính nối với hai điểm A và B như bên dưới. Điểm càng gần trục chính 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 vùng 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 và sử dụng hiệu ứng hoa sau xử lý và pha trộn với hình ảnh cuối cùng để có được á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 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 Lightaber

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 ra con đường mòn với các hình tam giác được tạo ra một cách linh động dựa trên chuyển động của thanh kiếm ánh sáng. Sau đó, những người hâm mộ này được truyền đến bộ xử lý hậu kỳ để cải thiện hình ảnh. Để 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

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 thành phần cuối cùng, chúng tôi phải xử lý ánh sáng xung quanh đường mòn có thể được tạo theo nhiều cách. Giải pháp mà chúng tôi do không đi vào chi tiết ở đây, vì lý do hiệu suất là hãy tạo chương trình đổ bóng cho vùng đệm này để tạo một cạnh nhẵn xung quanh 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ủ Chính sách bảo mật nội dung (CSP) thì bạn có thể áp dụng biện pháp này!

Cảnh chơi trò chơi