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

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

סיכום

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

סקירה כללית

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

אפשר לראות איך הדברים מסודרים בצורה נקייה אם כוללים סגנונות ולוגיקה מקבצים נפרדים. כדי לכלול את הסגנונות שלנו ב-Polymer אנחנו משתמשים בהצהרה include של Jade, כך שיש לנו CSS מובנה של תוכן הקובץ לאחר ההידור. רכיב הסקריפט sw-ui-logo.js בזמן הריצה.

יחסי תלות מודולריים עם Bower

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

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

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

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

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

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

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

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

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

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

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

בכל צינור עיבוד נתונים של גרפיקה תלת-ממדית, תמיד יש שלב אחרון שבו מוסיפים אפקטים מעל התמונה כולה, כסוג של שכבת-על. כאן בשלב שאחרי העיבוד, והוא כולל אפקטים כמו זוהר, קרניים עומק השדה, בוקה, טשטוש וכו'. האפקטים משולבים ומחילים על של האלמנטים השונים בהתאם לאופן שבו הסצנה נוצרה. ב-THREE.js אנחנו ליצור תוכנת הצללה (shader) מותאמת אישית לאחר העיבוד ב-JavaScript, או אנחנו יכולים לעשות זאת עם פולימר, בזכות המבנה של הורה-ילד.

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

נניח שאנחנו רוצים להסיר את אפקט DOF (עומק שדה) לשנות את הסדר של אפקטים של פריחה ושל דהייה בשוליים. כל מה שאנחנו צריכים לעשות הוא לערוך ההגדרה של המעבד (post-CPU) היא בערך כך:

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

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

חרב אור ב-THREE.js

THREE.js מפשט את הפרטים ברמה הנמוכה של WebGL ומאפשר לנו להתמקד בבעיה. הבעיה שלנו היא להילחם בכוחות הסער, נשק חם. אז בואו נבנה חרב אור.

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

הלהב

הלהב מורכב משני להבי משנה. פנימית וחיצונית. שתיהן הן רשתות THREE.js עם החומרים התואמים שלהן.

הלהב הפנימי

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

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

זוהר להב פנימי

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

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

};

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

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

להב חיצוני

שביל חרב האור

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

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

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

השביל המלא

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

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

שביל זוהר

סיכום

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

מהלכי משחק