ساخت شمشیر نور با پلیمر

اسکرین شات شمشیر نوری

خلاصه

چگونه از پلیمر برای ایجاد یک Lightsaber کنترل شده موبایل WebGL با کارایی بالا استفاده کردیم که ماژولار و قابل تنظیم است. ما برخی از جزئیات کلیدی پروژه خود را در https://lightsaber.withgoogle.com/ مرور می‌کنیم تا به شما در صرفه‌جویی در وقت کمک کنیم تا دفعه بعد که با گروهی از Stormtroopers عصبانی برخورد می‌کنید، خودتان را بسازید.

بررسی اجمالی

اگر می‌پرسید پلیمر یا اجزای وب چیست، ما فکر می‌کنیم که بهتر است با به اشتراک گذاشتن عصاره‌ای از یک پروژه واقعی شروع کنیم. در اینجا نمونه ای از صفحه فرود پروژه ما 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 کتابخانه ای است که به شما اجازه می دهد تا قدرت زیادی در مورد نحوه ساخت پروژه شما از عناصر سفارشی قابل استفاده مجدد داشته باشید. این به شما امکان می دهد از ماژول های مستقل و کاملاً کاربردی موجود در یک فایل 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) و قالب (فایل 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 در زمان اجرا اجرا می شود.

وابستگی های مدولار با Bower

معمولاً ما کتابخانه ها و سایر وابستگی ها را در سطح پروژه نگه می داریم. با این حال، در تنظیمات بالا یک bower.json مشاهده خواهید کرد که در پوشه عنصر قرار دارد: وابستگی سطح عنصر. ایده پشت این رویکرد این است که در شرایطی که عناصر زیادی با وابستگی‌های مختلف دارید، می‌توانیم مطمئن شویم که فقط وابستگی‌هایی را بارگیری می‌کنیم که واقعاً استفاده می‌شوند. و اگر عنصری را حذف کنید، نیازی نیست که حذف وابستگی آن را به خاطر بسپارید زیرا فایل bower.json را نیز حذف خواهید کرد که این وابستگی ها را اعلام می کند. هر عنصر به طور مستقل وابستگی های مربوط به خود را بارگذاری می کند.

با این حال، برای جلوگیری از تکراری شدن وابستگی ها، یک فایل .bowerrc را نیز در پوشه هر عنصر قرار می دهیم. این به bower می‌گوید که در کجا وابستگی‌ها را ذخیره کند، بنابراین ما می‌توانیم مطمئن شویم که فقط یکی در پایان در همان فهرست وجود دارد:

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

به این ترتیب اگر چندین عنصر THREE.js به عنوان یک وابستگی اعلام کنند، هنگامی که bower آن را برای عنصر اول نصب کرد و شروع به تجزیه عنصر دوم کرد، متوجه می‌شود که این وابستگی قبلاً نصب شده است و آن را دوباره دانلود یا تکرار نمی‌کند. به طور مشابه، تا زمانی که حداقل یک عنصر آن را در 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 بالا.

از جمله عناصر پلیمری

برای گنجاندن عناصر پلیمری، از واردات 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 فایل جداگانه تبدیل می شود. این به این معنی است که مرورگر درخواست های زیادی را باید انجام دهد، با از دست دادن عملکرد. مشابه فرآیند الحاق و کوچک‌سازی که برای ساخت Angular اعمال می‌کنیم، پروژه پلیمر را در پایان برای تولید «ولکانیزاسیون» می‌کنیم.

Vulcanize یک ابزار پلیمری است که درخت وابستگی را به یک فایل html تبدیل می‌کند و تعداد درخواست‌ها را کاهش می‌دهد. این به ویژه برای مرورگرهایی که به صورت بومی از اجزای وب پشتیبانی نمی کنند بسیار عالی است.

CSP (سیاست امنیتی محتوا) و پلیمر

هنگام توسعه برنامه های کاربردی وب ایمن، باید CSP را پیاده سازی کنید. CSP مجموعه ای از قوانین است که از حملات اسکریپت بین سایتی (XSS) جلوگیری می کند: اجرای اسکریپت ها از منابع ناامن، یا اجرای اسکریپت های درون خطی از فایل های HTML.

اکنون یک فایل .html بهینه شده، الحاق و کوچک شده که توسط Vulcanize تولید شده است، همه کدهای جاوا اسکریپت را به صورت خطی در قالبی غیر سازگار با CSP دارد. برای رفع این مشکل از ابزاری به نام Crisper استفاده می کنیم.

کریسپر اسکریپت های درون خطی را از یک فایل HTML جدا می کند و آنها را در یک فایل جاوا اسکریپت خارجی برای انطباق با CSP قرار می دهد. بنابراین ما فایل HTML ولکانیزه شده را از طریق کریسپر عبور می دهیم و در نهایت دو فایل داریم: elements.html و elements.js . داخل elements.html همچنین از بارگیری elements.js مراقبت می کند.

ساختار منطقی برنامه

در Polymer، عناصر می توانند هر چیزی باشند، از یک ابزار غیر بصری گرفته تا عناصر UI کوچک، مستقل و قابل استفاده مجدد (مانند دکمه ها) تا ماژول های بزرگتر مانند "صفحات" و حتی نوشتن برنامه های کامل.

ساختار منطقی سطح بالای برنامه
یک ساختار منطقی سطح بالا از برنامه ما که با عناصر پلیمری نشان داده شده است.

پس پردازش با پلیمر و معماری والد-فرزند

در هر خط لوله گرافیکی سه بعدی، همیشه آخرین مرحله وجود دارد که در آن افکت‌ها به عنوان نوعی پوشش روی کل تصویر اضافه می‌شوند. این مرحله پس از پردازش است، و شامل جلوه هایی مانند درخشش، پرتوهای خدا، عمق میدان، بوکه، تاری و غیره است. افکت ها با توجه به نحوه ساخت صحنه ترکیب شده و روی عناصر مختلف اعمال می شوند. در THREE.js می‌توانیم یک سایه‌زن سفارشی برای پس‌پردازش در جاوا اسکریپت ایجاد کنیم یا می‌توانیم این کار را با 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 جاوا اسکریپت استفاده می‌کنیم تا همه افکت‌ها را که به‌عنوان عناصر 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()

سپس، از data binding برای اتصال ویژگی‌های 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 اضافه کردیم که به شما امکان می‌دهد پاسخ تماس قبلی اضافه شده را حذف کنید.

یک Lightsaber در 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" )

};

درخشش تیغه بیرونی

برای درخشش بیرونی ما به یک رندربافر جداگانه رندر می‌شویم و از افکت بلوم پس از پردازش استفاده می‌کنیم و با تصویر نهایی ترکیب می‌کنیم تا درخشش مورد نظر را به دست آوریم. تصویر زیر سه منطقه مختلف را نشان می دهد که اگر می خواهید یک سابر مناسب داشته باشید، باید داشته باشید. یعنی هسته سفید، درخشش میانی مایل به آبی و درخشش بیرونی.

تیغه بیرونی

مسیر شمشیر نور

دنباله شمشیر نوری کلیدی برای جلوه کامل است، همانطور که در سری جنگ ستارگان دیده می شود. ما مسیر را با فن مثلث هایی ساختیم که به صورت پویا بر اساس حرکت شمشیر نوری ایجاد شده است. سپس این فن ها برای بهبود بصری بیشتر به پس پردازشگر منتقل می شوند. برای ایجاد هندسه فن، ما یک پاره خط داریم و بر اساس تبدیل قبلی و تبدیل فعلی آن، یک مثلث جدید در مش ایجاد می‌کنیم و قسمت دم را پس از یک طول مشخص رها می‌کنیم.

رد شمشیر نور سمت چپ
مسیر شمشیر نوری سمت راست

هنگامی که یک مش داریم، یک ماده ساده به آن اختصاص می دهیم و آن را به پس پردازشگر می دهیم تا جلوه ای صاف ایجاد کند. ما از همان جلوه شکوفه‌ای استفاده می‌کنیم که برای درخشش تیغه بیرونی اعمال کرده‌ایم و همانطور که می‌بینید یک مسیر صاف به دست می‌آوریم:

مسیر کامل

در اطراف مسیر بدرخشید

برای اینکه قطعه نهایی کامل شود، باید درخشش را در اطراف دنباله واقعی کنترل می‌کردیم، که می‌توانست به روش‌های مختلفی ایجاد شود. راه حل ما که در اینجا به جزئیات آن نمی پردازیم، به دلایل عملکرد، ایجاد یک سایه زن سفارشی برای این بافر بود که یک لبه صاف در اطراف یک گیره از رندربافر ایجاد می کند. سپس این خروجی را در رندر نهایی ترکیب می کنیم، در اینجا می توانید درخششی را که مسیر را احاطه کرده است ببینید:

مسیر با درخشش

نتیجه

پلیمر یک کتابخانه و مفهوم قدرتمند است (همانطور که WebComponents به طور کلی هستند). این فقط به شما بستگی دارد که با آن چه بسازید. این می تواند هر چیزی باشد، از یک دکمه UI ساده گرفته تا یک برنامه WebGL با اندازه کامل. در فصل‌های قبلی نکات و ترفندهایی را برای نحوه استفاده مؤثر از پلیمر در تولید و نحوه ساخت ماژول‌های پیچیده‌تر که عملکرد خوبی نیز دارند به شما نشان داده‌ایم. ما همچنین به شما نشان دادیم که چگونه در WebGL به یک شمشیر نوری زیبا برسید. بنابراین اگر همه اینها را ترکیب کردید، به یاد داشته باشید که قبل از استقرار در سرور تولید، عناصر پلیمری خود را Vulcanize کنید و اگر اگر می‌خواهید مطابق با CSP بمانید، استفاده از کریسپر را فراموش نکنید، ممکن است این نیرو با شما باشد!

بازی بازی