Cómo crear un sable de luz con polímero

Captura de pantalla con sable de luz

Resumen

Cómo usamos Polymer para crear un sable de luz controlado por un dispositivo móvil con WebGL de alto rendimiento, modular y configurable Revisamos algunos detalles clave de nuestro proyecto https://lightsaber.withgoogle.com/ para ayudarte a ahorrar tiempo cuando crees los tuyos la próxima vez que te encuentres con un grupo de Stormtroopers enojados.

Descripción general

Si te preguntas qué son Polymer o WebComponents, pensamos que lo mejor sería comenzar compartiendo un extracto de un proyecto real en funcionamiento. A continuación, puedes ver una muestra tomada de la página de destino de nuestro proyecto https://lightsaber.withgoogle.com. Es un archivo HTML normal, pero tiene cierta magia:

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

Por lo tanto, hoy en día existen muchas opciones para crear una aplicación basada en HTML5. API, frameworks, bibliotecas, motores de juegos, etc., a pesar de todas las opciones, es difícil obtener una configuración que sea una buena combinación entre el control de alto rendimiento de los gráficos y una estructura modular y escalabilidad limpias. Descubrimos que Polymer podía ayudarnos a mantener el proyecto organizado y, al mismo tiempo, permitir optimizaciones de rendimiento de bajo nivel, y diseñamos cuidadosamente la forma en que dividimos nuestro proyecto en componentes para aprovechar mejor las capacidades de Polymer.

Modularidad con Polymer

Polymer es una biblioteca que permite una gran cantidad de control sobre cómo se compila tu proyecto a partir de elementos personalizados reutilizables. Te permite usar módulos independientes y completamente funcionales que se encuentran en un solo archivo HTML. No solo contienen la estructura (lenguaje de marcado HTML), sino también la lógica y los estilos intercalados.

Mira el siguiente ejemplo:

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

Sin embargo, en un proyecto más grande, podría ser útil separar estos tres componentes lógicos (HTML, CSS, JS) y solo fusionarlos en el tiempo de compilación. Por lo tanto, una cosa que hicimos fue darle a cada elemento del proyecto su propia carpeta independiente:

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

Además, la carpeta de cada elemento tiene la misma estructura interna con directorios y archivos separados para la lógica (archivos de café), estilos (archivos scss) y plantilla (archivo jade).

Este es un ejemplo de elemento sw-ui-logo:

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

Si observas el archivo .jade, sucederá lo siguiente:

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

Puedes ver cómo se organizan los elementos de forma clara si incluyes los estilos y la lógica de archivos separados. Para incluir nuestros estilos en los elementos de Polymer, usamos la declaración include de Jade, por lo que tenemos contenido real de archivos CSS intercalados después de la compilación. El elemento de la secuencia de comandos sw-ui-logo.js se ejecutará en el entorno de ejecución.

Dependencias modulares con Bower

Por lo general, conservamos las bibliotecas y otras dependencias a nivel de proyecto. Sin embargo, en la configuración anterior, notarás un bower.json en la carpeta del elemento: dependencias a nivel del elemento. La idea detrás de este enfoque es que, en una situación en la que tienes muchos elementos con diferentes dependencias, podemos asegurarnos de cargar solo las dependencias que en realidad se usan. Si quitas un elemento, no es necesario que recuerdes quitar su dependencia, ya que también quitarás el archivo bower.json que declara estas dependencias. Cada elemento carga de manera independiente las dependencias que se relacionan con él.

Sin embargo, para evitar la duplicación de dependencias, también incluimos un archivo .bowerrc en la carpeta de cada elemento. Esto le indica a bower dónde almacenar las dependencias para que podamos asegurarnos de que haya solo una al final en el mismo directorio:

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

De esta manera, si varios elementos declaran THREE.js como una dependencia, una vez que Bower lo instala para el primer elemento y comienza a analizar el segundo, se dará cuenta de que esta dependencia ya está instalada y no la volverá a descargar ni la duplicará. De manera similar, se conservarán los archivos de dependencia siempre que haya al menos un elemento que aún los defina en su bower.json.

Una secuencia de comandos Bash busca todos los archivos bower.json en la estructura de elementos anidados. Luego, ingresa a estos directorios uno por uno y ejecuta bower install en cada uno de ellos:

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

Nueva plantilla de elemento rápido

Toma un poco de tiempo cada vez que quieres crear un elemento nuevo: generar la carpeta y la estructura de archivos básica con los nombres correctos. Por lo tanto, usamos Slush para escribir un generador de elementos simple.

Puedes llamar a la secuencia de comandos desde la línea de comandos:

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

Y se crea el nuevo elemento, que incluye toda la estructura del archivo y el contenido.

Definimos las plantillas para los archivos de elementos, p.ej., la plantilla de archivo .jade se ve de la siguiente manera:

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

El generador de Slush reemplaza las variables por rutas de acceso y nombres de elementos reales.

Usa Gulp para compilar elementos

Gulp mantiene el proceso de compilación bajo control. Y en nuestra estructura, para compilar los elementos, necesitamos que Gulp siga estos pasos:

  1. Compila los archivos .coffee de los elementos en .js
  2. Compila los archivos .scss de los elementos en .css
  3. Compila los archivos .jade de los elementos en .html y, luego, incorpora los archivos .css.

En más detalle:

Compilar los archivos .coffee de los elementos en .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')));
});

Para los pasos 2 y 3, usamos gulp y un complemento de brújula a fin de compilar scss en .css y .jade en .html, en un enfoque similar al 2 anterior.

Incluye elementos de polímero

Para incluir los elementos de Polymer, utilizamos importaciones 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">

Cómo optimizar elementos de Polymer para la producción

Un proyecto grande puede terminar teniendo muchos elementos Polymer. En nuestro proyecto, tenemos más de cincuenta. Si consideras que cada elemento tiene un archivo .js separado y que algunos tienen bibliotecas a las que se hace referencia, se convierte en más de 100 archivos independientes. Esto implica muchas solicitudes que debe realizar el navegador, con pérdida de rendimiento. De manera similar a un proceso de concatenación y reducción que aplicaríamos a una compilación de Angular, “vulcanizamos” el proyecto Polymer al final para su producción.

Vulcanize es una herramienta de Polymer que aplana el árbol de dependencias en un solo archivo HTML, lo que reduce la cantidad de solicitudes. Esta opción es muy útil para los navegadores que no admiten componentes web de forma nativa.

CSP (Política de Seguridad del Contenido) y Polymer

Cuando se desarrollan aplicaciones web seguras, se debe implementar la CSP. La CSP es un conjunto de reglas que evitan ataques de secuencias de comandos entre sitios (XSS): la ejecución de secuencias de comandos desde fuentes no seguras o la ejecución de secuencias de comandos intercaladas desde archivos HTML.

Ahora, el archivo .html optimizado, concatenado y reducido que genera Vulcanize tiene todo el código JavaScript intercalado en un formato que no cumple con CSP. Para abordar esto, usamos una herramienta llamada Crisper.

Crisper divide las secuencias de comandos intercaladas de un archivo HTML y las coloca en un solo archivo JavaScript externo para el cumplimiento de la CSP. Por lo tanto, pasamos el archivo HTML vulcanizado a través de Crisper y obtenemos dos archivos: elements.html y elements.js. Dentro de elements.html, también se encarga de cargar el objeto elements.js generado.

Estructura lógica de la aplicación

En Polymer, los elementos pueden ser desde una utilidad no visual hasta elementos de la IU pequeños, independientes y reutilizables (como botones) hasta módulos más grandes, como "páginas", e incluso crear aplicaciones completas.

Una estructura lógica de nivel superior de la aplicación
Es una estructura lógica de nivel superior de nuestra aplicación, representada con elementos Polymer.

Procesamiento posterior con Polymer y la arquitectura principal y secundaria

En cualquier canalización de gráficos 3D, siempre hay un último paso, en el que los efectos se agregan sobre la imagen completa como una especie de superposición. Este es el paso posterior al procesamiento e implica efectos como resplandores, rayos dios, profundidad de campo, bokeh, desenfoques, etc. Los efectos se combinan y se aplican a diferentes elementos según la forma en que se construya la escena. En THREE.js, podríamos crear un sombreador personalizado para el posprocesamiento en JavaScript o podemos hacerlo con Polymer, gracias a su estructura de elemento superior y secundario.

Si observas el código HTML del elemento posterior al procesador:

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

Especificamos los efectos como elementos Polymer anidados en una clase común. Luego, en sw-experience-postprocessor.js, haremos lo siguiente:

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

Usamos la función de HTML y el querySelectorAll de JavaScript para encontrar todos los efectos anidados como elementos HTML dentro del posprocesador, en el orden en que se especificaron. Luego, las iteramos y las agregamos al compositor.

Ahora, supongamos que queremos quitar el efecto DOF (profundidad de campo) y cambiar el orden de los efectos de floración y de viñeta. Todo lo que tenemos que hacer es editar la definición del posprocesador a algo como:

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

y la escena se ejecutará, sin cambiar ni una sola línea del código real.

Bucle de renderizado y bucle de actualización en Polymer

Con Polymer también podemos abordar las actualizaciones de renderización y motor con elegancia. Creamos un elemento timer que usa requestAnimationFrame y calcula valores como la hora actual (t) y el tiempo delta, que es el tiempo transcurrido desde el último fotograma (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()

Luego, usamos la vinculación de datos para vincular las propiedades t y dt a nuestro motor (experience.jade):

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

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

Además, escuchamos los cambios de t y dt en el motor y, cada vez que los valores cambien, se llamará a la función _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

Sin embargo, si te apetece FPS, es posible que quieras quitar la vinculación de datos de Polymer en el bucle de renderización para ahorrar algunos milisegundos necesarios para notificar los elementos sobre los cambios. Implementamos observadores personalizados de la siguiente manera:

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 función addUpdateListener acepta una devolución de llamada y la guarda en su array de devoluciones de llamada. Luego, en el bucle de actualización, iteramos cada devolución de llamada y la ejecutamos directamente con los argumentos dt y t, sin pasar por la vinculación de datos o la activación de eventos. Una vez que una devolución de llamada ya no esté activa, agregamos una función removeUpdateListener que te permite quitar una devolución de llamada agregada anteriormente.

Un sable de luz en THREE.js

THREE.js abstrae los detalles de bajo nivel de WebGL y permite que nos enfoquemos en el problema. Nuestro problema es luchar contra los Stormtroopers y necesitamos un arma. Creemos un sable de luz.

La hoja brillante es lo que diferencia a un sable de luz de cualquier arma antigua de dos manos. Se compone principalmente de dos partes: el haz y el rastro que se ve al moverlo. Lo construimos con una forma de cilindro brillante y un rastro dinámico que lo sigue a medida que el jugador se mueve.

La espada

La hoja está compuesta por dos hojas secundarias. Una interna y una externa. Ambas son mallas de THREE.js con sus respectivos materiales.

La hoja interior

Para el cuchillo interno usamos un material personalizado con un sombreador personalizado. Tomamos una línea creada por dos puntos y proyectamos la línea entre estos dos puntos en un plano. Este plano es básicamente lo que controlas cuando luchas con tu dispositivo móvil; le da la sensación de profundidad y orientación al sable.

Para crear la sensación de un objeto redondo brillante, observamos la distancia del punto ortogonal de cualquier punto en el plano desde la línea principal que une los dos puntos A y B, como se muestra a continuación. Cuanto más cerca esté un punto del eje principal, más brillante será.

Resplandor en la hoja interior

En la siguiente fuente, se muestra cómo calculamos un vFactor para controlar la intensidad en el sombreador de vértices y, luego, usarlo para combinarlo con la escena del sombreador de fragmentos.

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

};

El brillo de la hoja exterior

Para el resplandor externo, renderizamos en un búfer de renderización separado, usamos un efecto de bloom posterior al procesamiento y combinamos con la imagen final a fin de obtener el resplandor deseado. En la siguiente imagen, se muestran las tres regiones diferentes que necesitas si quieres un sable decente. es decir, el núcleo blanco, el resplandor azul azul medio y el resplandor externo.

Hoja exterior

Sendero del sable de luz

El rastro del sable de luz es clave para lograr el efecto completo, como se ve en la serie original de Star Wars. Hicimos el recorrido con un abanico de triángulos generados de forma dinámica según el movimiento del sable de luz. Luego, estos ventiladores se pasan al posprocesador para una mejor mejora visual. Para crear la geometría del ventilador, tenemos un segmento de línea y, en función de su transformación anterior y la transformación actual, generamos un nuevo triángulo en la malla y descartamos la parte de la cola después de una longitud determinada.

Sendero del sable de luz a la izquierda
Sendero del sable de luz a la derecha

Una vez que tenemos una malla, le asignamos un material simple y lo pasamos al posprocesador para crear un efecto fluido. Usamos el mismo efecto de floración que aplicamos al brillo de la hoja exterior y obtenemos un rastro suave como puedes ver:

El recorrido completo

Resplandor alrededor del camino

Para que la pieza final estuviera completa, tuvimos que controlar el resplandor alrededor de la pista real, que podía crearse de varias maneras. La solución que no vamos a detallar aquí, por razones de rendimiento, fue crear un sombreador personalizado para este búfer que crea un borde suave alrededor de una fijación del búfer de renderización. Luego, combinamos este resultado en la renderización final. Aquí puedes ver el resplandor que rodea el recorrido:

Sendero con brillo

Conclusión

Polymer es una biblioteca y un concepto potentes (al igual que los WebComponents en general). Depende de ti lo que hagas con él. Puede ser desde un simple botón de la IU hasta una aplicación WebGL de tamaño completo. En los capítulos anteriores, te mostramos algunas sugerencias y trucos para usar Polymer de manera eficiente en la producción y estructurar módulos más complejos que también funcionan bien. También te mostramos cómo lograr un sable de luz atractivo en WebGL. Por lo tanto, si combinas todo eso, recuerda Vulcanizar tus elementos Polymer antes de implementarlos en el servidor de producción y, si no te olvidas de usar Crisper, si quieres cumplir con CSP, puedes recurrir a la fuerza.

Partida de un videojuego