การสร้างไลท์เซเบอร์ด้วยโพลีเมอร์

ภาพหน้าจอของ Lightsaber

สรุป

วิธีที่เราใช้ Polymer สร้าง WebGL ประสิทธิภาพสูงที่ควบคุมในอุปกรณ์เคลื่อนที่ ไลท์เซเบอร์ที่สามารถปรับแต่งได้ เราตรวจสอบรายละเอียดสำคัญบางอย่างของโปรเจ็กต์ https://lightsaber.withgoogle.com/ เพื่อช่วยให้ประหยัดเวลาเมื่อสร้างดาบแสงของคุณเองในครั้งถัดไปที่พบกลุ่มสตอร์มทรูปเปอร์ที่โกรธ

ภาพรวม

หากสงสัยว่า Polymer หรือ WebComponents คืออะไร เราคิดว่าการเริ่มต้นด้วยการแชร์ข้อมูลบางส่วนจากโปรเจ็กต์ที่ใช้งานได้จริงน่าจะเป็นวิธีที่ดีที่สุด ต่อไปนี้คือตัวอย่างที่ตัดมาจากหน้า Landing Page ของโปรเจ็กต์ https://lightsaber.withgoogle.com ซึ่งเป็นไฟล์ HTML ปกติแต่มีเวทมนตร์อยู่ภายใน

<!-- Element-->
<dom-module id="sw-page-landing">
    <!-- Template-->
    <template>
    <style>
        <!-- include elements/sw/pages/sw-page-landing/styles/sw-page-landing.css-->
    </style>
    <div class="centered content">
        <sw-ui-logo></sw-ui-logo>
        <div class="connection-url-wrapper">
        <sw-t key="landing.type" class="type"></sw-t>
        <div id="url" class="connection-url">.</div>
        <sw-ui-toast></sw-ui-toast>
        </div>
    </div>
    <div class="disclaimer epilepsy">
        <sw-t key="disclaimer.epilepsy" class="type"></sw-t>
    </div>
    <sw-ui-footer state="extended"></sw-ui-footer>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-page-landing.js"></script>
</dom-module>

ปัจจุบันคุณมีตัวเลือกมากมาย เมื่อต้องการสร้างโฆษณา แอปพลิเคชันแบบ HTML5 API, เฟรมเวิร์ก, ไลบรารี, เครื่องมือเกม ฯลฯ แม้ว่าจะมีทางเลือกอื่นๆ มากมาย แต่ก็ยากที่จะหาการตั้งค่าที่ผสมผสานกันอย่างลงตัว ระหว่างการควบคุมกราฟิกประสิทธิภาพสูงและโมดูลที่สะอาดตา และความสามารถในการปรับขนาด เราพบว่า Polymer สามารถช่วยเราในการทำให้ จัดระเบียบแล้วในขณะที่ยังคงให้ประสิทธิภาพในระดับต่ำ การเพิ่มประสิทธิภาพสูงสุด และเราได้ออกแบบวิธีแจกแจงโครงการของเราอย่างละเอียด เป็นส่วนประกอบต่างๆ เพื่อใช้ความสามารถของ Polymer ให้เกิดประโยชน์สูงสุด

ความสามารถในการแยกโมดูลด้วยพอลิเมอร์

Polymer คือไลบรารีที่ช่วยให้ ศักยภาพอย่างมากในการสร้างโปรเจ็กต์จากองค์ประกอบที่กำหนดเองซึ่งนำมาใช้ใหม่ได้ ซึ่งช่วยให้คุณใช้โมดูลแบบสแตนด์อโลนที่ทำงานได้อย่างสมบูรณ์ที่มีอยู่ใน ไฟล์ HTML เดียว ไม่ได้มีเพียงโครงสร้าง (มาร์กอัป HTML) แต่ยังมี และตรรกะแบบอินไลน์

ดูตัวอย่างด้านล่าง

<link rel="import" href="bower_components/polymer/polymer.html">

<dom-module id="picture-frame">
    <template>
    <!-- scoped CSS for this element -->
    <style>
        div {
        display: inline-block;
        background-color: #ccc;
        border-radius: 8px;
        padding: 4px;
        }
    </style>
    <div>
        <!-- any children are rendered here -->
        <content></content>
    </div>
    </template>

    <script>
    Polymer({
        is: "picture-frame",
    });
    </script>
</dom-module>

แต่ในโปรเจ็กต์ที่ใหญ่กว่า คุณควรแยกทั้ง 3 องค์ประกอบนี้ออกจากกัน คอมโพเนนต์ (HTML, CSS, JS) และจะนำไปรวมขณะคอมไพล์เท่านั้น สิ่งหนึ่ง ที่เรากำหนดคือให้แต่ละองค์ประกอบในโครงการแยกโฟลเดอร์แยกกัน:

src/elements/
|-- elements.jade
`-- sw
    |-- debug
    |   |-- sw-debug
    |   |-- sw-debug-performance
    |   |-- sw-debug-version
    |   `-- sw-debug-webgl
    |-- experience
    |   |-- effects
    |   |-- sw-experience
    |   |-- sw-experience-controller
    |   |-- sw-experience-engine
    |   |-- sw-experience-input
    |   |-- sw-experience-model
    |   |-- sw-experience-postprocessor
    |   |-- sw-experience-renderer
    |   |-- sw-experience-state
    |   `-- sw-timer
    |-- input
    |   |-- sw-input-keyboard
    |   `-- sw-input-remote
    |-- pages
    |   |-- sw-page-calibration
    |   |-- sw-page-connection
    |   |-- sw-page-connection-error
    |   |-- sw-page-error
    |   |-- sw-page-experience
    |   `-- sw-page-landing
    |-- sw-app
    |   |-- bower.json
    |   |-- scripts
    |   |-- styles
    |   `-- sw-app.jade
    |-- system
    |   |-- sw-routing
    |   |-- sw-system
    |   |-- sw-system-audio
    |   |-- sw-system-config
    |   |-- sw-system-environment
    |   |-- sw-system-events
    |   |-- sw-system-remote
    |   |-- sw-system-social
    |   |-- sw-system-tracking
    |   |-- sw-system-version
    |   |-- sw-system-webrtc
    |   `-- sw-system-websocket
    |-- ui
    |   |-- experience
    |   |-- sw-preloader
    |   |-- sw-sound
    |   |-- sw-ui-button
    |   |-- sw-ui-calibration
    |   |-- sw-ui-disconnected
    |   |-- sw-ui-final
    |   |-- sw-ui-footer
    |   |-- sw-ui-help
    |   |-- sw-ui-language
    |   |-- sw-ui-logo
    |   |-- sw-ui-mask
    |   |-- sw-ui-menu
    |   |-- sw-ui-overlay
    |   |-- sw-ui-quality
    |   |-- sw-ui-select
    |   |-- sw-ui-toast
    |   |-- sw-ui-toggle-screen
    |   `-- sw-ui-volume
    `-- utils
        `-- sw-t

และโฟลเดอร์ของแต่ละองค์ประกอบมีโครงสร้างภายในเหมือนกัน ไดเรกทอรีและไฟล์สำหรับตรรกะ (ไฟล์กาแฟ) รูปแบบ (ไฟล์ SMS) และ (ไฟล์ Jade)

ต่อไปนี้คือตัวอย่างเอลิเมนต์ sw-ui-logo

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

และหากคุณตรวจสอบไฟล์ .jade ให้ทำดังนี้

// Element
dom-module(id='sw-ui-logo')

    // Template
    template
    style
        include elements/sw/ui/sw-ui-logo/styles/sw-ui-logo.css

    img(src='[[url]]')

    // Polymer element script
    script(src='scripts/sw-ui-logo.js')

คุณจะเห็นการจัดระเบียบสิ่งต่างๆ ให้ดูสะอาดตาได้ด้วยการใส่รูปแบบ และตรรกะจากคนละไฟล์ ให้รวมสไตล์ของเราไว้ในโพลิเมอร์ ที่เราใช้คำสั่ง include ของ Jade เราจึงมี CSS ในบรรทัดจริง เนื้อหาไฟล์หลังการคอมไพล์ องค์ประกอบสคริปต์ sw-ui-logo.js จะ ดำเนินการที่รันไทม์

การขึ้นต่อกันแบบโมดูลพร้อมเครื่องวัด

โดยปกติแล้วเราจะเก็บไลบรารีและทรัพยากร Dependency อื่นๆ ไว้ที่ระดับโปรเจ็กต์ แต่ในการตั้งค่าข้างต้น คุณจะเห็น bower.json ที่ โฟลเดอร์ขององค์ประกอบ: ทรัพยากร Dependency ระดับองค์ประกอบ แนวคิดเบื้องหลังวิธีการนี้ คือในสถานการณ์ที่คุณมีองค์ประกอบต่างๆ ที่มี เราสามารถตรวจสอบได้ เพื่อโหลดเฉพาะทรัพยากร Dependency ที่ ใช้งานจริง หากนำองค์ประกอบออก คุณก็ไม่ต้องจำว่า นำการอ้างอิงออก เนื่องจากคุณจะนำไฟล์ bower.json ออกด้วย ที่ประกาศทรัพยากร Dependency เหล่านี้ องค์ประกอบแต่ละรายการจะโหลด ทรัพยากร Dependency ที่เกี่ยวข้อง

อย่างไรก็ตาม เราได้รวมไฟล์ .bowerrc ไว้ด้วยเพื่อหลีกเลี่ยงความซ้ำซ้อนของทรัพยากร Dependency ในโฟลเดอร์ขององค์ประกอบแต่ละรายการด้วย คำสั่งนี้บอก Bower ว่าจะเก็บไฟล์ข้อมูลที่เกี่ยวข้องไว้ที่ใดเพื่อให้มั่นใจว่ามีไฟล์ข้อมูลที่เกี่ยวข้องเพียงไฟล์เดียวในไดเรกทอรีเดียวกัน

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

ด้วยวิธีนี้ หากองค์ประกอบหลายรายการประกาศ THREE.js เป็นทรัพยากร Dependency 1 ครั้ง Bower จะติดตั้งสำหรับองค์ประกอบแรก และเริ่มแยกวิเคราะห์องค์ประกอบที่ 2 ระบบจะทราบว่ามีการติดตั้งทรัพยากร Dependency นี้ไว้แล้วและจะไม่ โปรดดาวน์โหลดซ้ำหรือทำซ้ำ ในทำนองเดียวกัน ก็จะคงทรัพยากร Dependency นั้นไว้ ตราบใดที่ยังมีองค์ประกอบอย่างน้อย 1 อย่างที่ยังคงกำหนดไฟล์ bower.json

สคริปต์ Bash จะค้นหาไฟล์ bower.json ทั้งหมดในโครงสร้างองค์ประกอบที่ฝัง จากนั้นจะเข้าสู่ไดเรกทอรีเหล่านี้ทีละรายการและเรียกใช้ bower install ใน แต่ละแบบ ได้แก่

echo installing bower components...
modules=$(find /vagrant/app -type f -name "bower.json" -not -path "*node_modules*" -not -path "*bower_components*")
for module in $modules; do
    pushd $(dirname $module)
    bower install --allow-root -q
    popd
done

เทมเพลตองค์ประกอบใหม่แบบรวดเร็ว

การสร้างองค์ประกอบใหม่แต่ละครั้งจะใช้เวลาสักครู่ นั่นคือการสร้างโฟลเดอร์และโครงสร้างไฟล์พื้นฐานที่มีชื่อถูกต้อง เราจึงใช้ Slush เพื่อเขียนโปรแกรมสร้างองค์ประกอบอย่างง่าย

คุณสามารถเรียกสคริปต์จากบรรทัดคำสั่ง:

$ slush element path/to/your/element-name

ระบบจะสร้างองค์ประกอบใหม่ รวมถึงโครงสร้างและเนื้อหาไฟล์ทั้งหมด

เรากำหนดเทมเพลตสำหรับไฟล์องค์ประกอบต่างๆ เช่น เทมเพลตไฟล์ .jade มีลักษณะดังต่อไปนี้

// Element
dom-module(id='<%= name %>')

    // Template
    template
    style
        include elements/<%= path %>/styles/<%= name %>.css

    span This is a '<%= name %>' element.

    // Polymer element script
    script(src='scripts/<%= name %>.js')

เครื่องมือสร้าง Slush จะแทนที่ตัวแปรด้วยเส้นทางและชื่อจริงขององค์ประกอบ

การใช้ Gulp เพื่อสร้างองค์ประกอบ

Gulp ทำให้กระบวนการบิลด์อยู่ภายใต้การควบคุม และโครงสร้างของเราในการสร้างองค์ประกอบต่างๆ เราต้องใช้ Gulp โดยทำตามขั้นตอนต่อไปนี้

  1. คอมไพล์องค์ประกอบ .coffee ไฟล์ไปยัง .js
  2. คอมไพล์ไฟล์ .scss ขององค์ประกอบไปยัง .css
  3. คอมไพล์องค์ประกอบ .jade ไฟล์ไปยัง .html กำลังฝังไฟล์ .css

รายละเอียดเพิ่มเติมมีดังนี้

คอมไพล์ไฟล์ .coffee ขององค์ประกอบเป็น .js

gulp.task('elements-coffee', function () {
    return gulp.src(abs(config.paths.app + '/elements/**/*.coffee'))
    .pipe($.replaceTask({
        patterns: [{json: getVersionData()}]
    }))
    .pipe($.changed(abs(config.paths.static + '/elements'), {extension: '.js'}))
    .pipe($.coffeelint())
    .pipe($.coffeelint.reporter())
    .pipe($.sourcemaps.init())
    .pipe($.coffee({
    }))
    .on('error', gutil.log)
    .pipe($.sourcemaps.write())
    .pipe(gulp.dest(abs(config.paths.static + '/elements')));
});

สำหรับขั้นตอนที่ 2 และ 3 เราใช้ "เอว" และปลั๊กอินเข็มทิศในการคอมไพล์ scss เพื่อ .css และ .jade กับ .html ในลักษณะเดียวกับ 2 ด้านบน

รวมถึงองค์ประกอบ Polymer

ในการรวมองค์ประกอบ Polymer เราใช้การนำเข้า HTML

<link rel="import" href="elements.html">

<!-- Polymer -->
<link rel="import" href="../bower_components/polymer/polymer.html">

<!-- Custom elements -->
<link rel="import" href="sw/sw-app/sw-app.html">
<link rel="import" href="sw/system/sw-system/sw-system.html">
<link rel="import" href="sw/system/sw-routing/sw-routing.html">
<link rel="import" href="sw/system/sw-system-version/sw-system-version.html">
<link rel="import" href="sw/system/sw-system-environment/sw-system-environment.html">
<link rel="import" href="sw/pages/sw-page-landing/sw-page-landing.html">
<link rel="import" href="sw/pages/sw-page-connection/sw-page-connection.html">
<link rel="import" href="sw/pages/sw-page-calibration/sw-page-calibration.html">
<link rel="import" href="sw/pages/sw-page-experience/sw-page-experience.html">
<link rel="import" href="sw/ui/sw-preloader/sw-preloader.html">
<link rel="import" href="sw/ui/sw-ui-overlay/sw-ui-overlay.html">
<link rel="import" href="sw/ui/sw-ui-button/sw-ui-button.html">
<link rel="import" href="sw/ui/sw-ui-menu/sw-ui-menu.html">

กำลังเพิ่มประสิทธิภาพองค์ประกอบ Polymer เพื่อการผลิต

โปรเจ็กต์ขนาดใหญ่อาจมีองค์ประกอบโพลิเมอร์จำนวนมาก ใน เรามีมากกว่า 50 โปรเจ็กต์ ถ้าคุณพิจารณาว่าแต่ละองค์ประกอบมี แยก .js ไฟล์และบางไฟล์มีการอ้างอิงไลบรารี ไฟล์มีขนาดมากกว่า 100 ไฟล์แยกกัน ซึ่งหมายความว่าเบราว์เซอร์จะต้องส่งคำขอจำนวนมากและประสิทธิภาพจะลดลง ในลักษณะเดียวกับกระบวนการเชื่อมต่อและลดขนาด จะนำมาใช้กับงานสร้าง Angular ได้ เราจึงทำการ "แยกตัว" โครงการ Polymer ที่ สำหรับเวอร์ชันที่ใช้งานจริง

Vulcanize คือเครื่องมือพอลิเมอร์ที่ รวมต้นไม้ทรัพยากร Dependency ไว้เป็นไฟล์ HTML ไฟล์เดียว จำนวนคำขอ วิธีนี้เหมาะอย่างยิ่งสำหรับเบราว์เซอร์ที่ไม่ รองรับคอมโพเนนต์เว็บในตัว

CSP (นโยบายรักษาความปลอดภัยเนื้อหา) และ Polymer

เมื่อพัฒนาเว็บแอปพลิเคชันที่ปลอดภัย คุณจำเป็นต้องนำ CSP มาใช้ CSP เป็นชุดของกฎที่ป้องกันการโจมตีแบบ Cross-site Scripting (XSS) ดังนี้ การดำเนินการกับสคริปต์จากแหล่งที่มาที่ไม่ปลอดภัย หรือการเรียกใช้สคริปต์แบบอินไลน์ จากไฟล์ HTML

ตอนนี้ไฟล์ .html ไฟล์เดียวที่เพิ่มประสิทธิภาพ ต่อกัน และได้รับการย่อขนาดซึ่งสร้างขึ้นโดย Vulcanize จะมีโค้ด JavaScript ทั้งหมดอยู่ในรูปแบบที่ไม่ใช่ CSP ในการแก้ปัญหานี้ เราใช้เครื่องมือชื่อ Crisper

Crisper จะแยกสคริปต์ที่ติดทั่วหน้าเว็บออกจากไฟล์ HTML และใส่ไว้ในไฟล์ JavaScript ภายนอกไฟล์เดียวเพื่อให้เป็นไปตามข้อกำหนด CSP เราจึงเดินทางผ่านภูเขาไฟ HTML ผ่าน Crisper ได้ และลงท้ายด้วยไฟล์ 2 ไฟล์ ได้แก่ elements.html และ elements.js ใน elements.html ยังช่วยโหลด สร้างรายได้ elements.js

โครงสร้างตรรกะแอปพลิเคชัน

ใน Polymer องค์ประกอบอาจเป็นอะไรก็ได้ตั้งแต่ยูทิลิตีที่ไม่ใช่ภาพไปจนถึงองค์ประกอบ UI ขนาดเล็กแบบสแตนด์อโลนและนํากลับมาใช้ซ้ำได้ (เช่น ปุ่ม) ไปจนถึงโมดูลขนาดใหญ่ เช่น "หน้าเว็บ" และแม้แต่การคอมโพสิชันแอปพลิเคชันทั้งแอป

โครงสร้างเชิงตรรกะระดับบนสุดของแอปพลิเคชัน
โครงสร้างเชิงตรรกะระดับบนสุดของแอปพลิเคชันของเราที่แสดงด้วยองค์ประกอบ Polymer

การประมวลผลข้อมูลหลังการประมวลผลด้วยพอลิเมอร์และสถาปัตยกรรมหลัก-ย่อย

ในไปป์ไลน์กราฟิก 3 มิติ จะมีขั้นตอนสุดท้ายที่เอฟเฟกต์ จะปรากฏอยู่ด้านบนของทั้งภาพในรูปแบบของการวางซ้อน นี่คือ หลังการประมวลผล รวมถึงผลกระทบต่างๆ เช่น การเรืองแสง แสงสะท้อน ระยะชัดลึก โบเก้ เบลอ ฯลฯ ระบบจะนำเอฟเฟกต์ต่างๆ มารวมกันและนำไปใช้กับ องค์ประกอบต่างๆ ตามวิธีการสร้างฉาก ใน THREE.js เรา สามารถสร้างตัวปรับแสงเงาที่กำหนดเองสำหรับการประมวลผลภายหลังใน JavaScript หรือ เราทำแบบนี้ได้ด้วย Polymer ได้เพราะโครงสร้างแบบพ่อแม่ลูก

ลองดูที่โค้ด HTML ขององค์ประกอบหลังการประมวลผล

<dom-module id="sw-experience-postprocessor">
    <!-- Template-->
    <template>
    <sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
    <sw-experience-effect-dof class="effect"></sw-experience-effect-dof>
    <sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>

เราระบุเอฟเฟกต์เป็นองค์ประกอบ Polymer ที่ฝังอยู่ใต้คลาสทั่วไป จากนั้นให้ทำดังนี้ ใน sw-experience-postprocessor.js เราจะดำเนินการต่อไปนี้

effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects

เราใช้ฟีเจอร์ HTML และ querySelectorAll ของ JavaScript เพื่อค้นหา เอฟเฟ็กต์ที่ซ้อนเป็นองค์ประกอบ HTML ภายในโปรเซสเซอร์โพสต์ตามลำดับ ที่ได้ระบุไว้ จากนั้น เราจะทำซ้ำและเพิ่มลงในนักแต่งเพลง

คราวนี้สมมติว่าเราต้องการนำเอฟเฟ็กต์ DOF (ความลึกของฟิลด์) และ เปลี่ยนลำดับของเอฟเฟกต์การบานและขอบมืด เราเพียงแค่แก้ไข คำจำกัดความหลังผู้ประมวลผลข้อมูลเป็นดังนี้

<dom-module id="sw-experience-postprocessor">
    <!-- Template-->
    <template>
    <sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
    <sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>

แล้วฉากก็จะทำงานโดยไม่ต้องเปลี่ยนโค้ดจริงเลย

ลูปการแสดงภาพและอัปเดตลูปใน Polymer

นอกจากนี้ Polymer ยังปรับการแสดงผลและการอัปเดตเครื่องมือได้อย่างลงตัว เราได้สร้างองค์ประกอบ timer ที่ใช้ requestAnimationFrame และการคำนวณ ค่าต่างๆ เช่น เวลาปัจจุบัน (t) และเวลาเดลต้า - เวลาที่ผ่านไปจาก เฟรมสุดท้าย (dt):

Polymer
    is: 'sw-timer'

    properties:
    t:
        type: Number
        value: 0
        readOnly: true
        notify: true
    dt:
        type: Number
        value: 0
        readOnly: true
        notify: true

    _isRunning: false
    _lastFrameTime: 0

    ready: ->
    @_isRunning = true
    @_update()

    _update: ->
    if !@_isRunning then return
    requestAnimationFrame => @_update()
    currentTime = @_getCurrentTime()
    @_setT currentTime
    @_setDt currentTime - @_lastFrameTime
    @_lastFrameTime = @_getCurrentTime()

    _getCurrentTime: ->
    if window.performance then performance.now() else new Date().getTime()

จากนั้นเราจะใช้การเชื่อมโยงข้อมูลเพื่อเชื่อมโยงพร็อพเพอร์ตี้ t และ dt เข้ากับ เครื่องมือ (experience.jade):

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

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

และเราจะคอยฟังการเปลี่ยนแปลงของ t และ dt ในเครื่องมือ และเมื่อมีการเปลี่ยนแปลงค่า ระบบจะเรียกใช้ฟังก์ชัน _update ดังนี้

Polymer
    is: 'sw-experience-engine'

    properties:
    t:
        type: Number

    dt:
        type: Number

    observers: [
    '_update(t)'
    ]

    _update: (t) ->
    dt = @dt
    @_physics.update dt, t
    @_renderer.render dt, t

แต่หากต้องการใช้ FPS คุณอาจต้องนําข้อมูลของ Polymer ออก การเชื่อมโยงในการแสดงผลลูปเพื่อประหยัดเวลา 2-3 มิลลิวินาทีที่จำเป็นต่อการแจ้งเตือน องค์ประกอบเกี่ยวกับการเปลี่ยนแปลง เราติดตั้งใช้งานผู้สังเกตการณ์ที่กำหนดเองดังนี้

sw-timer.coffee:

addUpdateListener: (listener) ->
    if @_updateListeners.indexOf(listener) == -1
    @_updateListeners.push listener
    return

removeUpdateListener: (listener) ->
    index = @_updateListeners.indexOf listener
    if index != -1
    @_updateListeners.splice index, 1
    return

_update: ->
    # ...
    for listener in @_updateListeners
        listener @dt, @t
    # ...

ฟังก์ชัน addUpdateListener จะยอมรับการเรียกกลับและบันทึกไว้ในอาร์เรย์ callbacks จากนั้น ในลูปการอัปเดต เราจะทำซ้ำทุกครั้งที่ Callback เราจะดำเนินการด้วยอาร์กิวเมนต์ dt และ t โดยตรง เป็นการข้ามการเชื่อมโยงข้อมูลหรือ การเริ่มทำงานของเหตุการณ์ เมื่อการเรียกกลับไม่ได้ทำงานอีกต่อไป เราได้เพิ่มremoveUpdateListenerฟังก์ชันที่ช่วยให้คุณนําการเรียกกลับที่เพิ่มไว้ก่อนหน้านี้ออกได้

ไลท์เซเบอร์ใน THREE.js

THREE.js จะซ่อนรายละเอียดระดับล่างของ WebGL ไว้และช่วยให้เรามุ่งเน้นที่ปัญหาได้ และปัญหาของเราคือการต่อสู้กับสตอร์มทรูปเปอร์และเราต้องการอาวุธ มาสร้างไลท์เซเบอร์กัน

คมด้านที่เรืองแสงเป็นสิ่งที่ทำให้ดาบไลท์เซเบอร์แตกต่างจากดาบโบราณ อาวุธ 2 มือ โดยหลักๆ แล้วประกอบด้วย 2 ส่วน ได้แก่ ลำแสงและร่องรอยที่มองเห็นได้เมื่อเคลื่อน เราสร้างเมืองนี้ขึ้นมาโดยมีรูปทรงกระบอกสีสดใส และเส้นทางแบบไดนามิกที่ตามมาเมื่อผู้เล่นเคลื่อนที่

เดอะ เบลด

ใบมีดประกอบด้วยใบมีดย่อย 2 ใบ ทั้งภายในและภายนอก ทั้งคู่เป็น Mesh ของ THREE.js ที่มีเนื้อหาที่เกี่ยวข้อง

ใบมีดด้านใน

สำหรับใบมีดด้านใน เราใช้วัสดุที่ออกแบบเองพร้อมตัวปรับเฉดสีแบบกำหนดเอง พ นำเส้นที่สร้างขึ้นจากจุด 2 จุดมาแสดงเส้นแบ่งระหว่าง 2 จุดนี้ คะแนนเครื่องบิน โดยพื้นฐานแล้ว เครื่องบินนี้คือสิ่งที่คุณควบคุมได้ ต่อสู้ด้วยอุปกรณ์เคลื่อนที่ ซึ่งให้ความรู้สึกของความลึกและการวางแนว แทนดาบ

หากต้องการสร้างความรู้สึกของวัตถุกลมที่เรืองแสง เราจะดูระยะทางจุดตั้งฉากของจุดใดก็ได้บนระนาบจากเส้นหลักที่เชื่อมจุด A และ B ดังด้านล่าง ยิ่งจุดอยู่ใกล้ แกนหลักจะยิ่งสว่างมาก

ใบมีดด้านในเรืองแสง

แหล่งที่มาด้านล่างแสดงวิธีที่เราประมวลผล vFactor เพื่อควบคุมความเข้ม ในตัวปรับแสงเงา Vertex เพื่อใช้กลืนไปกับฉากใน ตัวปรับแสงเงา Fragment

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" )

};

แสงด้านนอกใบมีดเรืองแสง

สำหรับแสงที่สะท้อนออกมาด้านนอก เราจะเรนเดอร์ไปยังเรนเดอร์บัฟเฟอร์แยกต่างหาก และใช้เอฟเฟกต์ Bloom ในขั้นตอนหลังการประมวลผล แล้วผสมผสานกับภาพสุดท้ายเพื่อให้ได้แสงที่ต้องการ รูปภาพด้านล่างจะแสดง 3 ภูมิภาคที่ต่างกัน ถ้าคุณอยากได้ดาบที่ดูดี ได้แก่ แกนสีขาว ตรงกลาง เปล่งประกายสีฟ้าและแสงด้านนอก

ใบมีดด้านนอก

เส้นทางไลท์เซเบอร์

เส้นทางของไลท์เซเบอร์คือกุญแจสำคัญในการสร้างผลลัพธ์เต็มรูปแบบเช่นเดียวกับต้นฉบับที่เห็น ในซีรีส์ Star Wars เราสร้างร่องรอยด้วยรูปสามเหลี่ยมแบบพัดซึ่งสร้างขึ้นแบบไดนามิกตามการเคลื่อนไหวของดาบแสง แฟนๆ เหล่านี้จะ ไปยังโพสต์โปรเซสเซอร์เพื่อปรับปรุงภาพให้ดีขึ้น วิธีสร้าง เรขาคณิตของพัดลม เรามีส่วนของเส้นตรง และขึ้นอยู่กับการแปลงก่อนหน้านี้ และการเปลี่ยนรูปแบบปัจจุบันเราสร้างสามเหลี่ยมใหม่ใน Mesh โดยลดลง ออกจากส่วนหางหลังจากความยาวที่กำหนด

เส้นทางเดินไลท์เซเบอร์ทางซ้าย
ทางเดินไลท์เซเบอร์ทางขวา

เมื่อเรามีตาข่ายแล้ว เราจะกำหนดวัสดุง่ายๆ ให้และส่งต่อไปยัง เพื่อสร้างเอฟเฟ็กต์ที่ราบรื่น เราใช้เอฟเฟกต์ดอกไม้แบบเดียวกับที่ ที่ใช้กับใบมีดด้านนอกที่เรืองแสงและดูเส้นทางที่ราบรื่นอย่างที่เห็น ดังนี้

เส้นทางทั้งหมด

เรืองแสงรอบๆ ทางเดิน

เพื่อให้งานชิ้นสุดท้ายเสร็จสมบูรณ์ เราต้องจัดการกับการเรืองแสง เส้นทาง ซึ่งอาจสร้างได้หลายวิธี วิธีแก้ปัญหาที่เรา ไม่ได้ลงรายละเอียดในส่วนนี้ เนื่องจากเหตุผลด้านประสิทธิภาพคือการสร้าง ตัวปรับแสงเงาสำหรับบัฟเฟอร์นี้ ซึ่งสร้างขอบที่เรียบเนียนบริเวณที่หนีบของ Renderbuffer จากนั้นเราจะรวมเอาต์พุตนี้ในการแสดงภาพสุดท้าย ซึ่ง ดูแสงรอบๆ เส้นทางเดิน

ทางเดินที่มีแสงเรือง

บทสรุป

Polymer คือไลบรารีและแนวคิดที่มีประสิทธิภาพ (เช่นเดียวกับ WebComponents ทั่วไป) ขึ้นอยู่กับว่าคุณจะใช้แอปนี้ทำอะไร อาจเป็นอะไรก็ได้ตั้งแต่ ปุ่ม UI ง่ายๆ ไปยังแอปพลิเคชัน WebGL ขนาดเต็ม ในบทก่อนหน้า เราได้แสดงกลเม็ดเคล็ดลับในการใช้ Polymer อย่างมีประสิทธิภาพ จริงอยู่ที่วิธีจัดโครงสร้างโมดูล ที่ซับซ้อนมากขึ้นซึ่งมี เรายังได้บอกวิธีเล่นไลท์เซเบอร์ที่ดูดีใน WebGL ด้วย ดังนั้นเมื่อรวมทุกอย่างเข้าด้วยกันแล้ว อย่าลืม Vulcanize องค์ประกอบ Polymer ก่อนนำไปใช้งานในเซิร์ฟเวอร์เวอร์ชันที่ใช้งานจริง และอย่าลืมใช้ Crisper หากต้องการปฏิบัติตามข้อกำหนด CSP อยู่เสมอ

การเล่นเกม