יצירת חרב אור באמצעות פולימר

צילום מסך של חרב אור

סיכום

איך השתמשנו ב-Polymer כדי ליצור חרב (Lightsaber) מודולרית שמאפשרת לשלוט במכשירים ניידים ברמת ביצועים גבוהה. אנחנו בודקים כמה פרטים חשובים מהפרויקט שלנו, https://lightsaber.withgoogle.com/, כדי לעזור לכם לחסוך זמן כשאתם יוצרים את הפרויקט שלכם בפעם הבאה שתיתקלו בחבורת סערות כועסת.

סקירה כללית

אם אתם תוהים לגבי השימוש ב-Polymer או ב-WebComponents (רכיבי WebComponent), חשבנו שעדיף להתחיל בשיתוף תמצית מפרויקט קיים. הנה דוגמה שנלקחה מדף הנחיתה של הפרויקט שלנו 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, Frameworks, ספריות, מנועי משחקים וכו'. למרות כל האפשרויות, קשה להשיג הגדרה שתשלב בין שליטה על ביצועים גבוהים של גרפיקה לבין מבנה מודולרי וגמיש שניתן להתאים. גילינו ש-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>

אבל בפרויקט גדול יותר, כדאי להפריד בין שלושת הרכיבים הלוגיים האלה (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) ולתבנית (קובץ ירקן).

הנה רכיב 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 של ירקן כך שיש תוכן CSS בתוך השורה עצמה לאחר הידור. רכיב הסקריפט sw-ui-logo.js יפעל בזמן הריצה.

סוגי תלות מודולריים עם באוור

בדרך כלל אנחנו שומרים על הספריות ויחסי תלות אחרים ברמת הפרויקט. עם זאת, בהגדרות שלמעלה יופיע bower.json שנמצא בתיקייה של הרכיב: תלות ברמת הרכיב. הרעיון שעומד מאחורי הגישה הזו הוא שבמצב שבו יש הרבה אלמנטים עם יחסי תלות שונים, אנחנו יכולים לוודא שנטענו רק את יחסי התלות שבאמת נמצאים בשימוש. אם מסירים רכיב, אין צורך לזכור להסיר את התלות שלו כי מסירים גם את קובץ bower.json שמצהיר על יחסי התלות האלה. כל רכיב טוען בנפרד את יחסי התלות הקשורים אליו.

עם זאת, כדי למנוע כפילות של יחסי תלות, אנחנו כוללים גם קובץ .bowerrc בתיקייה של כל רכיב. כך נקבע לבאור איפה לאחסן יחסי תלות, כדי שנוכל לוודא שיש רק סוג אחד בסוף של אותה ספרייה:

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

כך, אם כמה רכיבים מצהירים על THREE.js כתלויות, ברגע שבאולר יתקין אותו עבור הרכיב הראשון ויתחיל לנתח את הרכיב השני, הוא יבין שהתלות הזו כבר מותקנת ולא תוריד אותה מחדש ולא תשכפל אותה. באופן דומה, קובצי התלות יישמרו כל עוד יש לפחות רכיב אחד שמגדיר אותם בקובץ 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 מאפשר לכם לשלוט בתהליך ה-build. ובמבנה שלנו, כדי ליצור את הרכיבים שנדרשים ב-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 שלמעלה.

כולל אלמנטים פולימריים

כדי לכלול בפועל את רכיבי הפולימר, אנחנו משתמשים בייבוא 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 קבצים נפרדים. זאת אומרת הרבה בקשות שהדפדפן צריך לשלוח, וכתוצאה מכך גורמות לפגיעה בביצועים. בדומה לתהליך שרשור והקטנה שנוכל להחיל על build של Angular, אנחנו "מגייסים" את פרויקט הפולימר שבסוף הייצור שלו.

Vulcanize הוא כלי פולימר שמשטח את עץ התלות בקובץ HTML אחד, ומפחית את מספר הבקשות. זה שימושי במיוחד לדפדפנים שלא תומכים ברכיבי אינטרנט במקור.

CSP (Content Security Policy) ופולימר

כשאתם מפתחים אפליקציות אינטרנט מאובטחות, צריך להטמיע CSP. CSP היא קבוצת כללים שמונעים מתקפות של סקריפטים חוצי אתרים (XSS): הפעלה של סקריפטים ממקורות לא בטוחים או הפעלת סקריפטים מוטבעים מקובצי HTML.

עכשיו, קוד ה-JavaScript בקובץ .html שעבר אופטימיזציה, משורשר ומוקטן שנוצר על ידי Vulcanize מכיל את כל קוד ה-JavaScript בפורמט שלא תואם ל-CSP. כדי לפתור את הבעיה אנחנו משתמשים בכלי שנקרא Crisper.

Crisper מפצל סקריפטים מוטבעים מקובץ HTML ומציב אותם לקובץ JavaScript חיצוני אחד, כדי לשמור על התאימות ל-CSP. אנחנו מעבירים את קובץ ה-HTML המגוחך דרך Crisper ובסוף מקבלים שני קבצים: elements.html ו-elements.js. ב-elements.html אפשר גם לטעון את ה-elements.js שנוצר.

מבנה לוגי של אפליקציות

בפולימר, האלמנטים יכולים להיות כל דבר, מכלי עזר לא חזותי, מרכיבי ממשק משתמש קטנים ועצמאיים ומיועדים לשימוש חוזר (כמו לחצנים) ועד למודולים גדולים יותר כמו "דפים", ואפילו חיבור של אפליקציות מלאות.

מבנה לוגי ברמה העליונה של האפליקציה
מבנה לוגי ברמה העליונה של האפליקציה שלנו, שמיוצג באמצעות אלמנטים של פולימרים.

עיבוד לאחר עיבוד עם ארכיטקטורת הורים-ילדים

בכל צינור עיבוד נתונים של גרפיקה בתלת-ממד, תמיד יש שלב אחרון שבו נוספים אפקטים על התמונה כולה, כסוג של שכבת-על. זהו השלב שאחרי העיבוד והוא כולל אפקטים כמו זוהר, קרניים בסגנון זוהר, עומק שדה, בוקה, טשטוש וכו'. האפקטים משולבים ומחילים על אלמנטים שונים בהתאם לאופן שבו הסצנה בנויה. ב-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>

אנחנו מגדירים את האפקטים כרכיבי פולימר מקוננים במחלקה משותפת. לאחר מכן, ב-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 בלולאת עיבוד כדי לחסוך כמה אלפיות השנייה שנדרשות כדי להודיע לרכיבים על השינויים. אנחנו מטמיעים את התצפית בהתאמה אישית באופן הבא:

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 מקבלת קריאה חוזרת ושומרת אותה במערך הקריאות החוזרות שלה. לאחר מכן, בלולאת העדכון, אנחנו מבצעים חזרה על כל קריאה חוזרת (callback) ומפעילים אותה ישירות באמצעות ארגומנטים מסוג dt ו-t, תוך עקיפת קישור הנתונים או הפעלת האירוע. הוספנו פונקציית removeUpdateListener שמאפשרת לבטל את הקריאה החוזרת (callback) שכבר השתמשתם בה בעבר.

חרב אור ב-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" )

};

הלהב החיצוני זוהר

כדי ליצור את הזוהר החיצוני, אנחנו מבצעים רינדור למאגר אחסון נפרד ומשתמשים באפקט הפריחה לאחר העיבוד ומתמזגים עם התמונה הסופית כדי לקבל את הזוהר הרצוי. התמונה הבאה מציגה את שלושת האזורים השונים שצריך להשתמש בהם כדי לשמור על חרב הגונה. כלומר, הליבה הלבנה, הזוהר הכחול האמצעי והזוהר החיצוני.

להב חיצוני

מסלול חרב האור

השובל של חרב האור הוא המפתח לאפקט המלא, כמו המקור שמוצג בסדרת מלחמת הכוכבים. השלמנו את השביל עם מאוורר של משולשים שנוצר באופן דינמי בהתאם לתנועה של חרב האור. לאחר מכן, המאווררים מועברים אל המעבד לעיבוד המידע לשיפור חזותי נוסף. כדי ליצור את הגיאומטריה של האוורור, יש לנו מקטע קו, ועל סמך הטרנספורמציה הקודמת שלו והטרנספורמציה הנוכחית, אנחנו יוצרים משולש חדש ברשת ומסירים את חלק הזנב אחרי אורך מסוים.

שובל של חרב האור משמאל
שביל של חרב אור ימינה

כשיש לנו רשת, אנחנו מקצים לה חומר פשוט ומעבירים אותו לאחר מכן לעיבוד כדי ליצור אפקט חלק. אנחנו משתמשים באותו אפקט פריחה שהחלנו על הזוהר החיצוני של הלהב ויוצרים שובל חלק כמו שאפשר לראות:

המסלול המלא

זוהרים סביב השביל

כדי שהיצירה הסופית תהיה מלאה, היינו צריכים להתמודד עם הזוהר מסביב לשביל האמיתי, שאותו ניתן ליצור בכמה דרכים. לכן, מסיבות שקשורות לביצועים, לא נפרט את הפתרון שלנו, הוא ליצור מארגן בהתאמה אישית למאגר הזה שיוצר קצה חלק מסביב למהדק של מאגר הנתונים הזמני. לאחר מכן נשלב את הפלט הזה בעיבוד הסופי וכאן תוכלו לראות את הזוהר שמקיף את השביל:

שביל עם זוהר

סיכום

Polymer הוא ספרייה וקונספט רבי-עוצמה (כמו WebComponents באופן כללי). ההחלטה שלך תלויה רק בך. זה יכול להיות כל דבר, מלחצן ממשק משתמש פשוט ועד אפליקציית WebGL בגודל מלא. בפרקים הקודמים הצגנו כמה טיפים וטריקים לשימוש יעיל בפולימר בסביבת הייצור, ואיך לבנות מודולים מורכבים יותר שמניבים ביצועים טובים. הראינו לך גם כיצד להשיג חרב אור נעימה ב-WebGL. לכן אם משלבים את כל הגורמים האלה, חשוב לזכור לפרוץ את רכיבי הפולימר לפני הפריסה לשרת הייצור. אם רוצים להמשיך להשתמש ב-Crisper כדי לעמוד בתאימות ל-CSP, ייתכן שהכוח יהיה איתכם!

מהלכי משחק