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

ภาพหน้าจอไลท์เซเบอร์

สรุป

วิธีที่เราใช้ Polymer สร้าง Lightsaber สมรรถนะสูงสำหรับอุปกรณ์เคลื่อนที่ด้วย 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, เฟรมเวิร์ก, ไลบรารี, Game Engines ฯลฯ แม้จะมีตัวเลือกมากมาย แต่การตั้งค่าที่ผสมผสานกันอย่างลงตัวระหว่างการควบคุมประสิทธิภาพกราฟิกประสิทธิภาพสูง โครงสร้างโมดูลาร์และความสามารถในการปรับขนาดที่สะอาดตานั้นทำได้ยาก เราพบว่า 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

และโฟลเดอร์ของแต่ละองค์ประกอบมีโครงสร้างภายในเหมือนกันโดยมีไดเรกทอรีและไฟล์แยกต่างหากสำหรับตรรกะ (ไฟล์กาแฟ) รูปแบบ (ไฟล์ scss) และเทมเพลต (ไฟล์ 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')

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

ทรัพยากร Dependency แบบโมดูลาร์กับ Bower

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

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

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

ด้วยวิธีนี้ หากองค์ประกอบหลายรายการประกาศว่า THREE.js เป็นทรัพยากร Dependency เมื่อเบราว์เซอร์ติดตั้งสำหรับองค์ประกอบแรกและเริ่มแยกวิเคราะห์องค์ประกอบที่ 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 เราใช้ gulp และปลั๊กอินเข็มทิศเพื่อคอมไพล์ scss ไปยัง .css และ .jade ไปยัง .html ด้วยวิธีเดียวกับที่ 2 ด้านบน

รวมถึงองค์ประกอบพอลิเมอร์

ในการรวมองค์ประกอบ 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 สำหรับการผลิต

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

Vulcanize เป็นเครื่องมือ Polymer ที่ย่อแผนผัง 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

พอลิเมอร์ยังช่วยให้เราเข้าถึงการแสดงภาพและการอัปเดตเครื่องมือได้อย่างสวยงาม เราสร้างองค์ประกอบ 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 จะยอมรับโค้ดเรียกกลับและบันทึกไว้ในอาร์เรย์การเรียกกลับ จากนั้นในลูปการอัปเดต เราจะทำซ้ำโค้ดเรียกกลับทั้งหมดและเรียกใช้ด้วยอาร์กิวเมนต์ dt และ t โดยตรง โดยข้ามการเชื่อมโยงข้อมูลหรือการเริ่มทำงานของเหตุการณ์ เมื่อไม่มีการเรียกใช้โค้ดเรียกกลับอีกต่อไป เราได้เพิ่มฟังก์ชัน removeUpdateListener ที่ให้คุณนําโค้ดเรียกกลับที่เพิ่มไว้ก่อนหน้านี้ออก

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

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

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

ใบมีด

ใบมีดประกอบด้วยใบพัด 2 ใบ ทั้งด้านในและด้านนอก ทั้ง 2 อย่างเป็น Mesh THREE.js พร้อมด้วยวัสดุที่เกี่ยวข้อง

ใบมีดด้านใน

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

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

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

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

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

};

ใบมีดชั้นนอก

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

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

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

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

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

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

ทุกเส้นทาง

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

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

เส้นทางมีแสงเรือง

บทสรุป

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

การเล่นเกม