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

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

ملخّص

كيف استخدمنا البوليمر لإنشاء نموذج WebGL عالي الأداء يتم التحكم فيه من خلال الأجهزة الجوّالة سيف ضوئي مكون من وحدات وقابل للتهيئة. نراجع بعض التفاصيل الأساسية من مشروعنا https://lightsaber.withgoogle.com/ لمساعدتك في توفير الوقت عند إنشاء تصميمك الخاص في المرة القادمة التي تواجه فيها مجموعة من جنود العواصف الغاضبين.

نظرة عامة

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

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

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

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

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

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

هيكل منطقي عالي المستوى للتطبيق
البنية المنطقية ذات المستوى الأعلى لتطبيقنا ممثّلة باستخدام عناصر Polymer

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

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

والآن، لنفترض أننا نريد إزالة تأثير عمق المجال (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

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

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

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

مسار السيف الضوئي إلى اليسار
مسار السيف الضوئي إلى اليمين

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

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

توهج حول المسار

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

درب متوهج

الخاتمة

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

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