Créer un sabre laser avec Polymer

Capture d'écran du sabre laser

Résumé

Découvrez comment nous avons utilisé Polymer pour créer un sabre laser laser, contrôlé par WebGL hautes performances sur mobile, modulaire et configurable. Nous passons en revue certains détails clés de notre projet https://lightsaber.withgoogle.com/ pour vous aider à gagner du temps lorsque vous créerez le vôtre la prochaine fois que vous rencontrerez une horde de Stormtroopers.

Présentation

Pour les Polymers ou WebComponents, nous avons pensé qu'il serait préférable de commencer par partager un extrait d'un projet en cours d'exécution. Voici un exemple extrait de la page de destination de notre projet https://lightsaber.withgoogle.com. Il s'agit d'un fichier HTML standard qui contient des éléments magiques:

<!-- 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>

Il existe donc aujourd'hui de nombreux choix pour créer une application HTML5. API, frameworks, bibliothèques, moteurs de jeu, etc. Malgré tous les choix, il est difficile d'obtenir une configuration qui offre un bon compromis entre contrôle des performances graphiques élevées et structure modulaire propre et évolutivité. Nous avons constaté que Polymer pouvait nous aider à organiser le projet tout en permettant une optimisation des performances de bas niveau. Nous avons soigneusement conçu la décomposition de notre projet en composants pour exploiter au mieux les fonctionnalités de Polymer.

Modularité avec polymère

Polymer est une bibliothèque qui vous permet de contrôler la manière dont votre projet est créé à partir d'éléments personnalisés réutilisables. Il vous permet d'utiliser des modules autonomes et entièrement fonctionnels contenus dans un seul fichier HTML. Ils contiennent non seulement la structure (balisage HTML), mais aussi les styles et la logique intégrés.

Examinez l'exemple ci-dessous:

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

Toutefois, sur un projet plus important, il peut être utile de séparer ces trois composants logiques (HTML, CSS, JS) et de les fusionner uniquement au moment de la compilation. Nous avons donc attribué à chaque élément du projet son propre dossier séparé:

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

Le dossier de chaque élément possède la même structure interne, avec des répertoires et des fichiers distincts pour la logique (fichiers café), les styles (fichiers scss) et le modèle (fichier Jade).

Voici un exemple d'élément sw-ui-logo:

sw-ui-logo/
|-- bower.json
|-- scripts
|   `-- sw-ui-logo.coffee
|-- styles
|   `-- sw-ui-logo.scss
`-- sw-ui-logo.jade

Et si vous examinez le fichier .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')

Vous pouvez voir comment les choses sont organisées de manière claire en incluant les styles et la logique de fichiers distincts. Pour inclure nos styles dans les éléments Polymer, nous utilisons l'instruction include de Jade. Nous avons donc le contenu du fichier CSS intégré après la compilation. L'élément de script sw-ui-logo.js s'exécutera au moment de l'exécution.

Dépendances modulaires avec Bower

Normalement, nous conservons les bibliothèques et d'autres dépendances au niveau du projet. Toutefois, dans la configuration ci-dessus, vous remarquerez un bower.json qui se trouve dans le dossier de l'élément: dépendances au niveau de l'élément. L'idée derrière cette approche est que si vous avez de nombreux éléments avec des dépendances différentes, nous pouvons nous assurer de ne charger que les dépendances réellement utilisées. De plus, si vous supprimez un élément, vous n'avez pas besoin de penser à supprimer sa dépendance, car vous avez également supprimé le fichier bower.json qui déclare ces dépendances. Chaque élément charge indépendamment les dépendances qui s'y rapportent.

Toutefois, pour éviter une duplication des dépendances, nous incluons également un fichier .bowerrc dans le dossier de chaque élément. Cela indique à quel emplacement stocker les dépendances afin de nous assurer qu'il n'y en a qu'une à la fin dans le même répertoire:

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

De cette façon, si plusieurs éléments déclarent THREE.js comme dépendance, une fois que ce dernier l'installe pour le premier élément et commence à analyser le deuxième, il se rend compte que cette dépendance est déjà installée et ne la télécharge pas ni ne la duplique. De même, il conserve ces fichiers de dépendance tant qu'au moins un élément le définit dans son bower.json.

Un script bash trouve tous les fichiers bower.json dans la structure des éléments imbriqués. Elle entre ensuite dans ces répertoires un par un et exécute bower install dans chacun d'eux:

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

Modèle d'élément "rapide"

La création d'un élément peut prendre un peu de temps: générer le dossier et la structure de fichiers de base avec les noms corrects. Nous utilisons donc Slush pour écrire un générateur d'éléments simple.

Vous pouvez appeler le script à partir de la ligne de commande:

$ slush element path/to/your/element-name

L'élément est créé avec toute la structure et le contenu des fichiers.

Nous avons défini des modèles pour les fichiers d'éléments. Par exemple, le modèle de fichier .jade se présente comme suit:

// 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')

Le générateur de Slush remplace les variables par les chemins et les noms réels des éléments.

Utiliser Gulp pour créer des éléments

Il garde le processus de compilation sous contrôle. Et dans notre structure, pour construire les éléments, Gulp doit suivre les étapes suivantes:

  1. Compilez les fichiers .coffee des éléments au format .js.
  2. Compilez les fichiers .scss des éléments au format .css.
  3. Compilez les fichiers .jade des éléments au format .html, en intégrant les fichiers .css.

Plus en détail:

Compilation des fichiers .coffee des éléments dans .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')));
});

Pour les étapes 2 et 3, nous utilisons gulp et un plug-in compass pour compiler scss en .css et .jade en .html, dans une approche semblable à la deuxième approche ci-dessus.

Y compris des éléments polymères

Pour inclure les éléments Polymer, nous utilisons des importations 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">

Optimiser les éléments Polymer pour la production

Un projet de grande envergure peut comporter de nombreux éléments Polymer. Dans notre projet, nous en avons plus de cinquante. Si vous considérez que chaque élément possède un fichier .js distinct et que certains contiennent des bibliothèques référencées, il deviendra plus de 100 fichiers distincts. Cela signifie que le navigateur doit effectuer un grand nombre de requêtes, avec une perte de performances. Comme pour un processus de concaténation et de réduction de la taille que nous appliquerions à une compilation Angular, nous "vulcaniser" le projet Polymer à la fin pour la production.

Vulcanize est un outil Polymer qui aplatit l'arborescence de dépendances en un seul fichier HTML, ce qui réduit le nombre de requêtes. Cela est particulièrement utile pour les navigateurs qui n'acceptent pas les composants Web de manière native.

CSP (Content Security Policy) et Polymer

Lorsque vous développez des applications Web sécurisées, vous devez implémenter CSP. CSP est un ensemble de règles qui empêchent les attaques de type script intersites (XSS) : exécution de scripts à partir de sources non sécurisées ou exécution de scripts intégrés à partir de fichiers HTML.

Désormais, le fichier .html optimisé, concaténé et minimisé généré par Vulcanize intègre tout le code JavaScript dans un format non conforme à CSP. Pour résoudre ce problème, nous utilisons un outil appelé Crisper.

Crisper divise les scripts intégrés à partir d'un fichier HTML et les place dans un seul fichier JavaScript externe à des fins de conformité avec CSP. Nous transmettons donc le fichier HTML vulcanisé via Crisper et nous obtenons deux fichiers: elements.html et elements.js. Dans elements.html, il se charge également de charger le elements.js généré.

Structure logique de l'application

Dans Polymer, il peut s'agir d'un utilitaire non visuel, de petits éléments d'interface utilisateur autonomes et réutilisables (tels que des boutons) ou de modules plus volumineux, comme des "pages", voire de la composition d'applications complètes.

Une structure logique de premier niveau de l&#39;application
Structure logique de premier niveau de notre application représentée par des éléments Polymer.

Post-traitement avec une architecture Polymer et parent-enfant

Dans tout pipeline de graphismes 3D, il y a toujours une dernière étape au cours de laquelle des effets sont ajoutés au-dessus de l'image entière en tant que type de superposition. Il s'agit de l'étape de post-traitement qui implique des effets tels que des halos, des rayons naturels, la profondeur de champ, un bokeh, des floutages, etc. Les effets sont combinés et appliqués à différents éléments en fonction de la construction de la scène. Dans THREE.js, nous pouvons créer un nuanceur personnalisé pour le post-traitement en JavaScript ou utiliser Polymer grâce à sa structure parent-enfant.

Si vous examinez le code HTML de l'élément de notre post-processeur:

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

Nous spécifions les effets en tant qu'éléments Polymer imbriqués dans une classe commune. Ensuite, dans sw-experience-postprocessor.js, nous effectuons cette opération:

effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects

Nous utilisons la fonctionnalité HTML et le querySelectorAll de JavaScript pour trouver tous les effets imbriqués en tant qu'éléments HTML dans le post-traitement, dans l'ordre dans lequel ils ont été spécifiés. Nous effectuons ensuite une itération et nous les ajoutons au compositeur.

Supposons maintenant que vous souhaitiez supprimer l'effet DOF (Profondeur de champ) et modifier l'ordre des effets de fleur et de vignette. Tout ce que nous avons à faire est de modifier la définition du post-processeur en quelque chose comme:

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

et la scène s'exécutera, sans modifier une seule ligne de code.

Boucle de rendu et boucle de mise à jour dans Polymer

Polymer nous permet également d'aborder le rendu et les mises à jour du moteur de manière élégante. Nous avons créé un élément timer qui utilise requestAnimationFrame et calcule des valeurs telles que l'heure actuelle (t) et le temps delta (temps écoulé depuis la dernière image) (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()

Ensuite, nous utilisons la liaison de données pour lier les propriétés t et dt à notre moteur (experience.jade):

sw-timer(
    t='{ % templatetag openvariable % }t}}',
    dt='{ % templatetag openvariable % }dt}}'
)

sw-experience-engine(
    t='[t]',
    dt='[dt]'
)

De plus, nous écoutons les modifications de t et dt dans le moteur, et chaque fois que les valeurs changent, la fonction _update est appelée:

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

Si vous avez soif de FPS, vous pouvez supprimer la liaison de données de Polymer dans la boucle de rendu pour gagner quelques millisecondes nécessaires pour informer les éléments des modifications. Nous avons implémenté des observateurs personnalisés comme suit:

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
    # ...

La fonction addUpdateListener accepte un rappel et l'enregistre dans son tableau de rappels. Ensuite, dans la boucle de mise à jour, nous effectuons une itération sur chaque rappel et nous l'exécutons directement avec les arguments dt et t, en contournant la liaison de données ou le déclenchement des événements. Une fois qu'un rappel n'est plus censé être actif, nous avons ajouté une fonction removeUpdateListener qui vous permet de supprimer un rappel ajouté précédemment.

Sabre laser dans THREE.js

THREE.js élimine le faible niveau de détail de WebGL et nous permet de nous concentrer sur le problème. Notre problème, c'est affronter les Stormtroopers et nous avons besoin d'une arme. Alors construisons un sabre laser.

Sa lame brillante différencie un sabre laser de toute ancienne arme à deux mains. Elle se compose principalement de deux parties: la poutre et la traînée visible en la déplaçant. Nous l'avons construit avec une forme cylindrique brillante et une piste dynamique qui le suit au fur et à mesure que le joueur se déplace.

The Blade

L'pale est composée de deux sous-palettes. Une couche intérieure et une extérieure. Les deux sont des maillages THREE.js avec leurs matériaux respectifs.

Lame intérieure

Pour la lame intérieure, nous avons utilisé un matériau personnalisé avec un nuanceur personnalisé. Nous prenons une ligne créée par deux points et projetons la ligne entre ces deux points dans un plan. Cet avion est essentiellement ce que vous contrôlez lorsque vous combattez avec votre mobile. Il donne au sabre une impression de profondeur et d'orientation.

Pour créer la sensation d'un objet rond et lumineux, nous regardons la distance entre les points orthogonaux de tout point du plan par rapport à la ligne principale reliant les deux points A et B, comme ci-dessous. Plus un point est proche de l'axe principal, plus il est lumineux.

Illumination de la pale intérieure

La source ci-dessous montre comment calculer un vFactor pour contrôler l'intensité du nuanceur de sommets, puis l'utiliser pour se fondre avec la scène du nuanceur de fragments.

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

};

Halo de lame extérieure

Pour le halo externe, nous effectuons le rendu dans un tampon de rendu distinct, nous utilisons un effet de fleur post-traitement et nous nous fondons avec l'image finale pour obtenir le halo souhaité. L'image ci-dessous montre les trois régions dont vous avez besoin si vous souhaitez un sabre adapté. À savoir le noyau blanc, le halo bleu-ish milieu et le halo externe.

Pâte extérieure

Sentier du sabre laser

La traînée du sabre laser est essentielle pour profiter pleinement de l'effet original de la série Star Wars. Nous avons créé la piste avec un éventail de triangles générés de manière dynamique en fonction du mouvement du sabre laser. Ces ventilateurs sont ensuite transmis au postprocesseur pour une amélioration visuelle ultérieure. Pour créer la géométrie du ventilateur, nous avons un segment de ligne et, en fonction de sa transformation précédente et de sa transformation actuelle, nous générons un nouveau triangle dans le maillage, en abandonnant la partie de queue après une certaine longueur.

Sentier du sabre laser à gauche
Sentier du sabre laser à droite

Une fois que nous avons un maillage, nous lui attribuons un matériau simple et le transmettons au postprocesseur pour créer un effet lisse. Nous utilisons le même effet de fleur que celui que nous avons appliqué à la brillance de la pale extérieure et obtenons une traînée lisse, comme vous pouvez le voir:

Le parcours complet

Brillez autour du sentier

Pour que la dernière partie soit terminée, nous avons dû gérer le halo autour du sentier proprement dit, ce qui peut être créé de différentes manières. La solution que nous n'allons pas détailler ici pour des raisons de performances consistait à créer un nuanceur personnalisé pour ce tampon afin de créer un bord lisse autour d'une pince du tampon de rendu. Nous combinons ensuite cette sortie dans le rendu final, et vous pouvez voir ici le halo entourant le sentier:

Sentier lumineux

Conclusion

Polymer est une bibliothèque et un concept puissants (identique aux composants WebComponents en général). Vous seul décidez de ce que vous en faites. Il peut s'agir d'un simple bouton d'interface utilisateur ou d'une application WebGL en taille réelle. Dans les chapitres précédents, nous vous avons donné quelques conseils et astuces pour utiliser efficacement Polymer en production et structurer des modules plus complexes, également performants. Nous vous avons également montré comment obtenir un beau sabre laser dans WebGL. Par conséquent, si vous combinez tout cela, n'oubliez pas de Vulcaniser vos éléments Polymer avant de les déployer sur le serveur de production et si vous n'oubliez pas d'utiliser Crisper pour assurer la conformité avec CSP, vous pouvez compter sur vous !

Séquence de jeu