إنشاء سيف ضوئي باستخدام البوليمر

لقطة شاشة للسيف الضوئي

ملخّص

طريقتنا في استخدام البوليمر لإنشاء سيف ضوئي عالي الأداء يتحكّم فيه بتقنية 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. واجهات برمجة التطبيقات وأُطر العمل والمكتبات ومحركات الألعاب وما إلى ذلك. على الرغم من جميع الخيارات، من الصعب الحصول على إعداد يمزج بشكل جيد بين التحكم في الأداء العالي للرسومات والبنية النموذجية النظيفة والقابلية للتوسع. ووجدنا أن البوليمر يمكن أن تساعدنا في الحفاظ على تنظيم المشروع مع السماح بتحسينات الأداء منخفضة المستوى، ولقد صممنا بعناية الطريقة التي قسّمنا بها مشروعنا إلى مكونات للاستفادة بشكل أفضل من قدرات البوليمر.

الوحدة النمطية باستخدام البوليمر

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 من Jade، وبالتالي نحصل على محتوى ملف CSS المضمّن الفعلي بعد التجميع. سيتم تنفيذ عنصر النص البرمجي sw-ui-logo.js في وقت التشغيل.

التبعيات النموذجية مع باور

عادةً ما نحتفظ بالمكتبات والتبعيات الأخرى على مستوى المشروع. ومع ذلك، في عملية الإعداد أعلاه، ستلاحظ وجود علامة bower.json في التبعيات على مستوى العنصر لمجلد العنصر: العنصر. الفكرة وراء هذا النهج هي أنه في حالة وجود الكثير من العناصر ذات التبعيات المختلفة، يمكننا التأكد من تحميل التبعيات المستخدمة في الواقع فقط. وإذا أزلت عنصرًا، لن تحتاج إلى تذكُّر إزالة تبعياته لأنّك أيضًا قد أزلت ملف bower.json الذي يعلن عن هذه التبعيات. يقوم كل عنصر بشكل مستقل بتحميل التبعيات المتعلقة به.

ومع ذلك، لتجنُّب تكرار التبعيات، نُضمِّن ملف .bowerrc في مجلد كل عنصر أيضًا. هذا يخبر مقدم الطلب مكان تخزين التبعيات حتى نتمكن من التأكد من وجود واحدة فقط في النهاية في نفس الدليل:

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

بهذه الطريقة، إذا أعلنت عدة عناصر عن أنّ السمة THREE.js هي تبعية، وبمجرد أن يثبّتها Looker للعنصر الأول ويبدأ في تحليل العنصر الثاني، سيدرك أنّ هذه التبعية قد تم تثبيتها مسبقًا ولن تتم إعادة تنزيلها أو تكرارها. وبالمثل، ستحتفظ بملفات التبعية هذه ما دام هناك عنصر واحد على الأقل يحدّدها في عنصر 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 لاتباع الخطوات التالية:

  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 الوحيد المحسّن والتسلسلي والمصغَّر الذي تم إنشاؤه من خلال Sense، على كل رموز JavaScript المضمَّنة بتنسيق غير متوافق مع سياسة أمان المحتوى (CSP). لحل هذه المشكلة، نستخدم أداة تُعرف باسم Crisper.

يقسّم Crisper النصوص البرمجية المضمّنة من ملف HTML ويضعها في ملف JavaScript خارجي واحد للامتثال لسياسة CSP. إذًا، نمرر ملف HTML المبرمَج عبر Crisper وانتهى الأمر بملفان: elements.html وelements.js. وفي elements.html، يتم أيضًا تحميل elements.js الذي تم إنشاؤه.

البنية المنطقية للتطبيق

في Polymer، يمكن أن تكون العناصر أي شيء بدءًا من فائدة غير مرئية إلى عناصر واجهة مستخدم صغيرة مستقلة وقابلة لإعادة الاستخدام (مثل الأزرار) إلى وحدات أكبر مثل "الصفحات" وحتى إنشاء التطبيقات الكاملة.

هيكل منطقي عالي المستوى للتطبيق
يشير ذلك إلى بنية منطقية عالية المستوى لتطبيقنا يتم تمثيلها بعناصر بوليمر.

المعالجة اللاحقة باستخدام البوليمر وبنية الشركة الرئيسية الفرعية

في أي مسار للرسومات الثلاثية الأبعاد، هناك دائمًا خطوة أخيرة حيث تتم إضافة التأثيرات فوق الصورة بأكملها كنوع من التراكب. هذه هي خطوة ما بعد المعالجة، وتتضمن تأثيرات مثل التوهج والأشعة الإلهية وعمق الحقل وضباب خفيف والتمويه وما إلى ذلك. ويتم دمج التأثيرات وتطبيقها على عناصر مختلفة وفقًا لكيفية تصميم المشهد. في 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>

وسيتم تشغيل المشهد، دون تغيير سطر واحد من الكود الفعلي.

حلقة العرض والتحديث في البوليمر

وباستخدام البوليمر، يمكننا أيضًا التعامل مع تعديلات العرض والمحرّكات بشكل أنيق. أنشأنا عنصر 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

إذا كنت تريد زيادة عدد اللقطات في الثانية، يمكنك إزالة ربط بيانات البوليمر في حلقة العرض لحفظ بضع مللي ثانية لازمة لإعلام العناصر بالتغييرات. لقد طبّقنا المراقبين المخصّصين على النحو التالي:

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 مع المواد الخاصة بها.

النصل الداخلي

بالنسبة إلى النصل الداخلي، استخدمنا مادة مخصّصة مع أداة تظليل مخصّصة. نأخذ خطًا يتم إنشاؤه من خلال نقطتين ونعرض الخط بين هاتين النقطتين على مستوى. هذه الطائرة هي أساسًا ما يمكنك التحكم به عندما تقاتل باستخدام هاتفك الجوّال، وتمنحك إحساسًا بالعمق والاتجاه للسيف.

لخلق إحساس بجسم لامع مستدير، ننظر إلى مسافة النقطة المتعامدة لأي نقطة على المستوى من الخط الرئيسي الذي يربط بين النقطتين "أ" و"ب" كما يلي. كلما اقتربت النقطة من المحور الرئيسي، زادت سطوعها.

توهّج الشفرة الداخلية

يوضّح المصدر أدناه كيفية احتساب 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. لقد قطعنا النور باستخدام مروحة من المثلّثات التي تم إنشاؤها ديناميكيًا بالاستناد إلى حركة السيف الضوئي. بعد ذلك، يتم تمرير هذه المراوح إلى المعالج اللاحق لإجراء تحسينات مرئية إضافية. لإنشاء هندسة المروحة، لدينا جزء من الخط واستنادًا إلى تحويله السابق والتحويل الحالي، ننشئ مثلثًا جديدًا في الشبكة، ونحذف جزء الذيل بعد طول معين.

مسار &quot;السيف الضوئي&quot; إلى اليسار
مسار السيف الضوئي الأيمن

بعد إنشاء الشبكة المتداخلة، نخصّص مادة بسيطة لها ونمررها إلى المعالج اللاحق للحصول على تأثير سلس. نستخدم نفس تأثير التفتح الذي طبقناه على لمعان النصل الخارجي للحصول على مسار سلس كما ترى:

المسار الكامل

توهّج حول الطريق

لكي تكون القطعة النهائية مكتملة، كان علينا التعامل مع التوهج حول الممر الفعلي، والذي يمكن إنشاؤه بعدة طرق. لم ندخل في التفاصيل هنا - لأسباب تتعلق بالأداء - كان الحل الذي توصلنا إليه هو إنشاء تظليل مخصّص لهذا المخزن المؤقت ينشئ حوافًا سلسة حول مشبك للمخزن المؤقت. بعد ذلك، يتم دمج هذه النتائج في العرض النهائي، هنا يمكنك رؤية اللمعان الذي يحيط بالممر:

مسار مع لمعان

الخاتمة

يعد Polymer مكتبة ومفهومًا قويًا (مثل مكونات الويب بشكل عام). الأمر متروك لك فقط فيما تحقق منه. ويمكن أن يكون أي شيء بدءًا من زر واجهة مستخدم بسيط إلى تطبيق WebGL كامل الحجم. أوضحنا لك في الفصول السابقة بعض النصائح والحيل حول كيفية استخدام البوليمر بكفاءة في الإنتاج وكيفية تصميم وحدات أكثر تعقيدًا تحقق أداءً جيدًا أيضًا. أوضحنا لك أيضًا كيفية الحصول على سيف ضوئي جميل المظهر في WebGL. لذلك، إذا جمعت كل ذلك، تذكّر أنّه عليك إجراء تحسينات على عناصر البوليمر قبل نشرها في خادم الإنتاج. وإذا كنت لا تنسَ استخدام Crisper، إذا كنت تريد الحفاظ على توافق CSP، عليك بذل كل ما بوسعك.

فيديو يصوّر أسلوب اللعب