สรุป
วิธีที่เราใช้ 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 โดยทำตามขั้นตอนต่อไปนี้
- คอมไพล์องค์ประกอบ
.coffee
ไฟล์ไปยัง.js
- คอมไพล์ไฟล์
.scss
ขององค์ประกอบไปยัง.css
- คอมไพล์องค์ประกอบ
.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 ขนาดเล็กแบบสแตนด์อโลนและนํากลับมาใช้ซ้ำได้ (เช่น ปุ่ม) ไปจนถึงโมดูลขนาดใหญ่ เช่น "หน้าเว็บ" และแม้แต่การคอมโพสิชันแอปพลิเคชันทั้งแอป
การประมวลผลข้อมูลหลังการประมวลผลด้วยพอลิเมอร์และสถาปัตยกรรมหลัก-ย่อย
ในไปป์ไลน์กราฟิก 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 อยู่เสมอ