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

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

ملخّص

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

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

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

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

ما بعد المعالجة باستخدام بنية البوليمر والبنية الأساسية والفرعية

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

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

يستخلص 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" )

};

لمعان النصل الخارجي

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

النصل الخارجي

مسار السيف الضوئي

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

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

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

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

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

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

درب متوهج

الخاتمة

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

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