पॉलिमर के साथ शमशेर-ए-रोशनी बनाना

Lightsaber का स्क्रीनशॉट

खास जानकारी

हमने Polymer का इस्तेमाल करके, WebGL मोबाइल से कंट्रोल होने वाली बेहतर प्रदर्शन करने वाली Lightsaber कैसे बनाई. यह मॉड्यूलर और कॉन्फ़िगर करने लायक है. हमने अपने प्रोजेक्ट https://lightsaber.withgoogle.com/ की कुछ अहम जानकारी की समीक्षा की है. इससे आपको ऐंग्री स्टॉर्मट्रूपर के साथ अगली बार जुड़ने पर, अपना समय बचाने में मदद मिलेगी.

खास जानकारी

अगर आपको पता है कि पॉलिमर या वेबकॉम्पोनेंट क्या है, तो हमारे हिसाब से काम करने वाले किसी प्रोजेक्ट से एक्सट्रैक्ट करना सबसे अच्छा रहेगा. यह रहा हमारे प्रोजेक्ट के लैंडिंग पेज से लिया गया एक नमूना https://lightsaber.withgoogle.com. यह एक सामान्य एचटीएमएल फ़ाइल है, लेकिन इसमें कुछ कमाल है:

<!-- 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 पर आधारित ऐप्लिकेशन बनाने के लिए आपके पास आज के समय में कई विकल्प होते हैं. एपीआई, फ़्रेमवर्क, लाइब्रेरी, गेम इंजन वगैरह. सभी विकल्पों के बावजूद ऐसा सेटअप पाना मुश्किल है जो ग्राफ़िक की बेहतरीन परफ़ॉर्मेंस, साफ़ मॉड्यूलर स्ट्रक्चर और बढ़ाए जा सकने की योग्यता पर बेहतर कंट्रोल हो. हमने पाया कि Polymer से हमें प्रोजेक्ट को व्यवस्थित रखने में मदद मिल सकती है. साथ ही, इससे परफ़ॉर्मेंस को भी ऑप्टिमाइज़ किया जा सकता है. साथ ही, हमने अपने प्रोजेक्ट को छोटे-छोटे कॉम्पोनेंट में बाँटा, ताकि पॉलिमर की क्षमताओं का ज़्यादा से ज़्यादा फ़ायदा लिया जा सके.

पॉलीमर के साथ मॉड्यूलेरिटी

पॉलीमर एक ऐसी लाइब्रेरी है जो फिर से इस्तेमाल किए जा सकने वाले कस्टम एलिमेंट से अपने प्रोजेक्ट को बनाने के तरीके पर काफ़ी असर डालती है. इसकी मदद से, एक ही एचटीएमएल फ़ाइल में मौजूद स्टैंडअलोन और पूरी तरह से काम करने वाले मॉड्यूल इस्तेमाल किए जा सकते हैं. इनमें सिर्फ़ स्ट्रक्चर (एचटीएमएल मार्कअप) के साथ-साथ इनलाइन स्टाइल और लॉजिक भी शामिल होते हैं.

नीचे दिया गया उदाहरण देखें:

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

हालांकि, किसी बड़े प्रोजेक्ट पर, इन तीन लॉजिकल कॉम्पोनेंट (एचटीएमएल, सीएसएस, 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

हर एलिमेंट के फ़ोल्डर का अंदरूनी स्ट्रक्चर एक जैसा है. इसमें अलग-अलग डायरेक्ट्री और लॉजिक (कॉफ़ी फ़ाइलें), स्टाइल (एससीएस फ़ाइलें), और टेंप्लेट (जेड फ़ाइल) के लिए फ़ाइलें हैं.

यहां 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 स्टेटमेंट का इस्तेमाल करते हैं. इसलिए, कंपाइलेशन के बाद हमारे पास असल इनलाइन सीएसएस फ़ाइल कॉन्टेंट उपलब्ध है. sw-ui-logo.js स्क्रिप्ट एलिमेंट, रनटाइम के दौरान काम करेगा.

बोवर के साथ मॉड्यूलर डिपेंडेंसी

आम तौर पर, हम लाइब्रेरी और दूसरी डिपेंडेंसी को प्रोजेक्ट लेवल पर रखते हैं. हालांकि, ऊपर दिए गए सेटअप में आपको एक bower.json दिखेगा, जो एलिमेंट के फ़ोल्डर में है: एलिमेंट लेवल डिपेंडेंसी. इस तरीके के पीछे मकसद यह है कि ऐसी स्थिति में जहां आपके पास अलग-अलग निर्भरता वाले कई एलिमेंट होते हैं, वहां हम सिर्फ़ उन डिपेंडेंसी को लोड कर सकते हैं जो असल में इस्तेमाल होती हैं. साथ ही, अगर किसी एलिमेंट को हटाया जाता है, तो आपको उसकी डिपेंडेंसी हटाने की ज़रूरत नहीं है. ऐसा इसलिए, क्योंकि आपने इन डिपेंडेंसी का एलान करने वाली bower.json फ़ाइल भी हटा दी होगी. हर एलिमेंट, खुद से जुड़ी डिपेंडेंसी लोड करता है.

हालांकि, डिपेंडेंसी के दोहराव से बचने के लिए हम हर एलिमेंट के फ़ोल्डर में एक .bowerrc फ़ाइल भी शामिल करते हैं. इससे ब्राउज़र को यह पता चलता है कि डिपेंडेंसी को कहां सेव करना है, ताकि हम यह पक्का कर सकें कि किसी डायरेक्ट्री के आखिर में सिर्फ़ एक ही डायरेक्ट्री हो:

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

इस तरह, अगर एक से ज़्यादा एलिमेंट, THREE.js को डिपेंडेंसी के तौर पर बताते हैं, तो बॉर इसे पहले एलिमेंट के लिए इंस्टॉल करके दूसरे एलिमेंट को पार्स करना शुरू कर देता है. इससे पता चलता है कि यह डिपेंडेंसी पहले से ही इंस्टॉल है. इसलिए, इसे न तो फिर से डाउनलोड किया जाएगा और न ही डुप्लीकेट किया जाएगा. इसी तरह, यह उस डिपेंडेंसी फ़ाइल को तब तक बनाए रखेगा, जब तक कि bower.json में कम से कम एक एलिमेंट मौजूद हो जो उसे तय करता हो.

बैश स्क्रिप्ट, नेस्ट किए गए एलिमेंट के स्ट्रक्चर में सभी 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')

स्लश जनरेटर, वैरिएबल को असल एलिमेंट पाथ और नामों से बदल देता है.

एलिमेंट बनाने के लिए गल्प का इस्तेमाल करना

Gulp, बिल्ड प्रोसेस को कंट्रोल में रखता है. हमने अपनी संरचना में, Gulp की मदद से बनाए गए चीज़ों को बनाने के लिए इन चरणों को पूरा किया:

  1. एलिमेंट की .coffee फ़ाइलों को .js में कंपाइल करें
  2. एलिमेंट की .scss फ़ाइलों को .css में कंपाइल करें
  3. .css फ़ाइलें एम्बेड करते हुए, एलिमेंट की .jade फ़ाइलों को .html में कंपाइल करें.

ज़्यादा जानकारी के लिए:

एलिमेंट की .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')));
});

दूसरे और तीसरे चरण में, हम scss को .css में और .jade से .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">

पॉलिमर एलिमेंट को प्रोडक्शन के लिए ऑप्टिमाइज़ करना

एक बड़े प्रोजेक्ट में कई पॉलिमर एलिमेंट हो सकते हैं. हमारे प्रोजेक्ट में, पचास से ज़्यादा हैं. अगर आपको लगता है कि हर एलिमेंट में एक अलग .js फ़ाइल है और कुछ एलिमेंट में लाइब्रेरी का रेफ़रंस दिया गया है, तो वे 100 से ज़्यादा अलग-अलग फ़ाइलें बन जाती हैं. इसका मतलब है कि परफ़ॉर्मेंस को कम करने के लिए, ब्राउज़र को कई अनुरोध करने होंगे. इसी तरह, हम ऐंगुलर बिल्ड को जोड़ने और छोटा करने की प्रोसेस का इस्तेमाल करते हैं. इसी तरह, हम प्रोडक्शन के आखिर में पॉलीमर प्रोजेक्ट को "वल्कनाइज़" करते हैं.

Vulcanize एक पॉलिमर टूल है, जो डिपेंडेंसी ट्री को एक एचटीएमएल फ़ाइल में फ़्लैंट करता है. इससे अनुरोधों की संख्या कम हो जाती है. यह उन ब्राउज़र के लिए खास तौर पर बहुत अच्छा है जो मूल रूप से वेब कॉम्पोनेंट के साथ काम नहीं करते.

सीएसपी (कॉन्टेंट की सुरक्षा के बारे में नीति) और पॉलिमर

सुरक्षित वेब ऐप्लिकेशन बनाते समय, आपको सीएसपी लागू करना होता है. सीएसपी नियमों का एक ऐसा सेट है जो क्रॉस-साइट स्क्रिप्टिंग (XSS) हमलों को रोकता है. जैसे: असुरक्षित सोर्स से स्क्रिप्ट लागू करना या एचटीएमएल फ़ाइलों से इनलाइन स्क्रिप्ट को लागू करना.

अब Vulcanize से जनरेट की गई, ऑप्टिमाइज़ की गई, जोड़ी गई, और छोटी की गई .html फ़ाइल में सभी JavaScript कोड इनलाइन मौजूद हैं. ये सीएसपी के मुताबिक नहीं हैं. इस समस्या को ठीक करने के लिए, हम क्रिस्टर नाम के एक टूल का इस्तेमाल करते हैं.

क्रिस्पर, इनलाइन स्क्रिप्ट को एचटीएमएल फ़ाइल से अलग-अलग करता है और उन्हें एक बाहरी JavaScript फ़ाइल में डाल देता है, ताकि सीएसपी का पालन किया जा सके. इसलिए, हम क्रिस्पर के ज़रिए वल्कनाइज़्ड एचटीएमएल फ़ाइल भेजते हैं और आखिर में दो फ़ाइलें होती हैं: elements.html और elements.js. elements.html के अंदर यह जनरेट किए गए elements.js को लोड करने का भी काम करता है.

ऐप्लिकेशन का लॉजिकल स्ट्रक्चर

पॉलिमर में, एलिमेंट किसी नॉन-विज़ुअल यूटिलिटी से लेकर छोटे, स्टैंडअलोन और फिर से इस्तेमाल किए जा सकने वाले यूज़र इंटरफ़ेस (यूआई) एलिमेंट (जैसे बटन) से लेकर "पेज" जैसे बड़े मॉड्यूल तक और यहां तक कि पूरे ऐप्लिकेशन तैयार करने तक भी हो सकते हैं.

ऐप्लिकेशन का टॉप-लेवल का लॉजिकल स्ट्रक्चर
हमारे ऐप्लिकेशन का टॉप-लेवल का लॉजिकल स्ट्रक्चर, जिसे पॉलीमर एलिमेंट से दिखाया गया है.

पॉलिमर और पैरंट-चाइल्ड आर्किटेक्चर की मदद से पोस्ट प्रोसेसिंग

किसी भी 3D ग्राफ़िक पाइपलाइन में, हमेशा एक आखिरी चरण होता है, जहां पूरी तस्वीर के ऊपर एक तरह के ओवरले के रूप में इफ़ेक्ट जोड़े जाते हैं. यह प्रोसेस होने के बाद का चरण है. इसमें, चमक, गॉड-रे, फ़ील्ड की गहराई, बोकेह, ब्लर वगैरह जैसे इफ़ेक्ट शामिल होते हैं. सीन बनाने के तरीके के हिसाब से, अलग-अलग एलिमेंट पर इफ़ेक्ट जोड़े और लागू किए जाते हैं. THREE.js में, हम JavaScript में पोस्ट-प्रोसेसिंग के लिए कस्टम शेडर बना सकते हैं. या हम Polymer के साथ ऐसा कर सकते हैं, इसकी पैरंट-चाइल्ड संरचना की वजह से.

अगर आप हमारे पोस्ट-प्रोसेसर के एलिमेंट एचटीएमएल कोड को देखें:

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

हम इफ़ेक्ट को एक सामान्य क्लास के तहत नेस्ट किए गए पॉलीमर एलिमेंट के रूप में तय करते हैं. फिर, sw-experience-postprocessor.js में हम यह करते हैं:

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

हम पोस्ट प्रोसेसर में एचटीएमएल एलिमेंट के तौर पर नेस्ट किए गए सभी इफ़ेक्ट को ढूंढने के लिए, एचटीएमएल सुविधा और JavaScript के querySelectorAll का इस्तेमाल करते हैं. इसके बाद, हम उन चीज़ों की समीक्षा करते हैं और उन्हें कंपोज़र के साथ जोड़ते हैं.

अब, मान लीजिए कि हम 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 (फ़्रेम प्रति सेकंड) की ज़रूरत है, तो हो सकता है कि आप रेंडर लूप में मौजूद पॉलिमर की डेटा बाइंडिंग को हटाना चाहें. इससे एलिमेंट को बदलावों के बारे में सूचना देने में लगने वाले मिलीसेकंड की बचत होगी. हमने कस्टम ऑब्ज़र्वर को इस तरह लागू किया:

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 के कम लेवल की जानकारी को हटा देता है और समस्या पर ज़्यादा ध्यान दे पाता है. और हमारी समस्या स्टॉर्मट्रूपर से लड़ रही है और हमें एक हथियार की ज़रूरत है. आइए, एक लाइटसेबर बनाते हैं.

चमकदार ब्लेड, लाइटसेबर को किसी भी पुराने दो हाथ वाले हथियार से अलग करता है. यह मुख्य रूप से दो भागों से बना होता है: बीम और पगडंडी जो इसे घुमाते समय दिखाई देती है. हमने इसे एक चमकदार सिलेंडर के आकार और एक डाइनैमिक ट्रेल के साथ बनाया है, जो खिलाड़ी के साथ-साथ चलता है.

द ब्लेड

ब्लेड में दो सब ब्लेड होते हैं. एक अंदरूनी और बाहरी हिस्सा. दोनों अपने-अपने मटीरियल के साथ THREE.js मेश हैं.

इनर ब्लेड

अंदरूनी ब्लेड के लिए, हमने कस्टम शेडर वाले कस्टम मटीरियल का इस्तेमाल किया. हम दो पॉइंट से बनी लाइन लेते हैं और हवाई जहाज़ पर इन दो पॉइंट के बीच लाइन प्रोजेक्ट करते हैं. यह एक प्लेन है, जिसे मोबाइल से लड़ते समय कंट्रोल किया जाता है. इससे सॉबर को गहराई और ओरिएंटेशन का पता चलता है.

गोल आकार में चमकने वाली चीज़ का एहसास कराने के लिए, हम हवाई जहाज़ पर मौजूद किसी भी पॉइंट की ऑर्थोगोनल पॉइंट की दूरी को देखते हैं. यह दूरी, दो पॉइंट 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" )

};

द आउटर ब्लेड ग्लो

बाहरी चमक के लिए हम एक अलग रेंडरबफ़र में रेंडर करते हैं और पोस्ट-प्रोसेसिंग ब्लूम इफ़ेक्ट का इस्तेमाल करते हैं और मनपसंद चमक पाने के लिए फ़ाइनल इमेज के साथ ब्लेंड करते हैं. नीचे दी गई इमेज में तीन अलग-अलग इलाके दिखाए गए हैं. अगर आपको एक बढ़िया शर्बत चाहिए, तो आपकी ज़रूरत होती है. इन लाइटों का नाम है, व्हाइट कोर, बीच में ब्लू-इश चमक, और बाहरी चमक.

आउटर ब्लेड

लाइटसेबर ट्रेल

लाइटसेबर की पगडंडी पर पूरी तरह से असर डालते हैं, जैसा कि Star Wars सीरीज़ में देखा गया है. हमने त्रिभुजों के फ़ैन के साथ इस रास्ते को बनाया, जो लाइटसेबर की गतिविधि पर आधारित डाइनैमिक रूप से जनरेट किया गया था. इसके बाद, इन प्रशंसकों को विज़ुअल को बेहतर बनाने के लिए पोस्टप्रोसेसर को भेज दिया जाता है. फ़ैन की ज्यामिति बनाने के लिए, हमारे पास लाइन सेगमेंट होता है. इसके पिछले ट्रांसफ़ॉर्म और मौजूदा ट्रांसफ़ॉर्म के आधार पर, हम मेश में एक नया ट्रायएंगल बनाते हैं. साथ ही, एक तय लंबाई के बाद पूंछ वाले हिस्से को छोड़ देते हैं.

शमशेर-ए-रोशनी वाला रास्ता बाईं ओर
दाईं ओर मौजूद लाइटसेबर ट्रेल

मेश मिल जाने के बाद, हम उसके लिए एक सामान्य मटीरियल असाइन करते हैं और उसे पोस्टप्रोसेसर को भेज देते हैं, ताकि खरीदारों को आसानी से काम करने में मदद मिल सके. हम उसी ब्लूम इफ़ेक्ट का इस्तेमाल करते हैं जिसे हमने बाहरी ब्लेड चमक के लिए इस्तेमाल किया है. इससे आपको एक स्मूद ट्रेल मिलती है, जैसा कि आप देख सकते हैं:

पूरा रास्ता

रास्ते के आस-पास की रोशनी

अंतिम भाग को पूरा करने के लिए हमें वास्तविक ट्रेल के आस-पास चमक को संभालना था, जिसे कई तरीकों से बनाया जा सकता था. हमने इस बफ़र के लिए एक कस्टम शेडर बनाया है, जो रेंडरबफ़र के क्लैंप के चारों ओर एक चिकना किनारा बनाता है. फिर हम इस आउटपुट को फ़ाइनल रेंडर में मिलाते हैं. यहां आपको ट्रेल के आस-पास होने वाली चमक दिखाई देगी:

चमक के साथ पगडंडी

नतीजा

Polymer एक असरदार लाइब्रेरी और कॉन्सेप्ट है (जैसा कि वेबकॉम्पोनेंट आम तौर पर काम करते हैं). यह आपके ऊपर निर्भर करता है कि आप इससे क्या बनाते हैं. यह कुछ भी हो सकता है. जैसे, एक सामान्य यूज़र इंटरफ़ेस (यूआई) बटन से लेकर, फ़ुल साइज़ के WebGL ऐप्लिकेशन. पिछले चैप्टर में, हमने पॉलीमर के प्रोडक्शन को बेहतर तरीके से इस्तेमाल करने के बारे में कुछ सुझाव और तरकीबें बताई हैं. साथ ही, ऐसे जटिल मॉड्यूल भी बताए गए हैं जो बेहतर परफ़ॉर्म कर सकते हैं. हमने आपको WebGL में अच्छी दिखने वाली लाइटसेबर पाने का तरीका भी दिखाया. इसलिए, अगर इन सभी चीज़ों को मिलाया जाता है, तो प्रोडक्शन सर्वर पर डिप्लॉय करने से पहले, अपने पॉलिमर एलिमेंट को वल्कनाइज़ करना न भूलें. अगर आपको सीएसपी का पालन करना है, तो क्रिपर का इस्तेमाल करना न भूलें.

गेम प्ले