Lichtschwert mit Polymer erstellen

Screenshot des Lichtschwerts

Zusammenfassung

So haben wir mit Polymer ein modulares und konfigurierbares, leistungsstarkes WebGL-Lichtschwert entwickelt, das von einem Mobilgerät gesteuert wird. Wir gehen einige wichtige Details unseres Projekts https://lightsaber.withgoogle.com/ durch, damit Sie beim nächsten Mal, wenn Sie auf eine Gruppe wütender Sturmtruppen treffen, Zeit sparen können.

Überblick

Wenn Sie sich nicht sicher sind, was Polymer oder WebComponents sind, dachten wir, dass es am besten wäre, mit einem Auszug aus einem tatsächlich laufenden Projekt zu beginnen. Hier sehen Sie ein Beispiel von der Landingpage unseres Projekts https://lightsaber.withgoogle.com. Es ist eine normale HTML-Datei, hat aber ein gewisses Maß an Magie:

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

Es gibt heutzutage viele Möglichkeiten, eine HTML5-basierte Anwendung zu erstellen. APIs, Frameworks, Bibliotheken, Game-Engines usw. Trotz all der Optionen ist es schwierig, eine Einrichtung zu finden, die eine gute Mischung aus Kontrolle über eine hohe Grafikleistung und sauberer modularer Struktur und Skalierbarkeit bietet. Wir fanden heraus, dass Polymer uns dabei helfen könnte, das Projekt zu organisieren und gleichzeitig Leistungsoptimierungen auf niedriger Ebene zu ermöglichen. Außerdem haben wir unser Projekt sorgfältig in Komponenten unterteilt, um die Funktionen von Polymer optimal zu nutzen.

Modularität mit Polymer

Polymer ist eine Bibliothek, die viel Kontrolle darüber bietet, wie Ihr Projekt aus wiederverwendbaren benutzerdefinierten Elementen erstellt wird. Damit können Sie eigenständige, voll funktionsfähige Module verwenden, die in einer einzelnen HTML-Datei enthalten sind. Sie enthalten nicht nur die Struktur (HTML-Markup), sondern auch Inline-Stile und -Logik.

Sehen Sie sich das folgende Beispiel an:

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

Bei einem größeren Projekt kann es jedoch hilfreich sein, diese drei logischen Komponenten (HTML, CSS, JS) zu trennen und nur bei der Kompilierung zusammenzuführen. Also haben wir für jedes Element im Projekt einen eigenen Ordner erstellt:

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

Der Ordner jedes Elements hat die gleiche interne Struktur mit separaten Verzeichnissen und Dateien für Logik (Kaffeedateien), Stile (SCS-Dateien) und Vorlage (Jade-Datei).

Hier ein Beispiel für ein sw-ui-logo-Element:

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

Und so sieht die Datei .jade so aus:

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

Sie können sehen, wie die Dinge übersichtlich organisiert sind, indem Sie Stile und Logik aus separaten Dateien einbeziehen. Um unsere Stile in unsere Polymer-Elemente aufzunehmen, verwenden wir die include-Anweisung von Jade, sodass wir nach der Kompilierung die tatsächlichen Inhalte der Inline-CSS-Dateien haben. Das Skriptelement sw-ui-logo.js wird zur Laufzeit ausgeführt.

Modulare Abhängigkeiten mit Bower

Normalerweise behalten wir Bibliotheken und andere Abhängigkeiten auf Projektebene. In der obigen Einrichtung sehen Sie jedoch ein bower.json, das sich im Ordner des Elements befindet: Abhängigkeiten auf Elementebene. Die Idee dahinter ist, dass wir in einer Situation, in der viele Elemente mit unterschiedlichen Abhängigkeiten vorhanden sind, dafür sorgen können, dass nur die tatsächlich verwendeten Abhängigkeiten geladen werden. Wenn Sie ein Element entfernen, müssen Sie nicht daran denken, seine Abhängigkeit zu entfernen, da Sie auch die Datei bower.json entfernt haben, die diese Abhängigkeiten deklariert. Jedes Element lädt die zugehörigen Abhängigkeiten unabhängig voneinander.

Um eine Duplizierung von Abhängigkeiten zu vermeiden, fügen wir jedoch auch eine .bowerrc-Datei in den Ordner jedes Elements ein. Dadurch wird festgelegt, wo Abhängigkeiten gespeichert werden sollen, damit wir sicherstellen können, dass am Ende im selben Verzeichnis nur eine davon vorhanden ist:

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

Wenn mehrere Elemente THREE.js als Abhängigkeit deklarieren, stellt er nach der Installation für das erste Element und dem Parsen des zweiten Elements fest, dass die Abhängigkeit bereits installiert ist und nicht noch einmal heruntergeladen oder dupliziert wird. Ebenso werden die Abhängigkeitsdateien beibehalten, solange es mindestens ein Element gibt, das sie noch in der bower.json definiert.

Ein Bash-Skript findet alle bower.json-Dateien in der Struktur verschachtelter Elemente. Anschließend wird nacheinander diese Verzeichnisse eingegeben und in jedem der Verzeichnisse bower install ausgeführt:

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

Schnelle Vorlage für neue Elemente

Jedes Mal, wenn Sie ein neues Element erstellen möchten, nimmt es etwas Zeit in Anspruch, den Ordner und die grundlegende Dateistruktur mit den richtigen Namen zu generieren. Daher verwenden wir Slush, um einen einfachen Elementgenerator zu schreiben.

Sie können das Skript über die Befehlszeile aufrufen:

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

Das neue Element wird erstellt, einschließlich der gesamten Dateistruktur und des Inhalts.

Wir haben Vorlagen für die Elementdateien definiert. Die Dateivorlage .jade sieht beispielsweise so aus:

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

Der Slush-Generator ersetzt die Variablen durch tatsächliche Elementpfade und -namen.

Gulp zum Erstellen von Elementen verwenden

Gulp hält den Build-Prozess unter Kontrolle. Um die Elemente zu erstellen, die Gulp in unserer Struktur benötigt, gehen wir so vor:

  1. Kompilieren Sie die .coffee-Dateien der Elemente in .js.
  2. Kompilieren Sie die .scss-Dateien der Elemente in .css.
  3. Kompilieren Sie die .jade-Dateien der Elemente in .html und betten Sie die .css-Dateien ein.

Näheres dazu:

Die .coffee-Dateien der Elemente werden in .js kompiliert.

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')));
});

In den Schritten 2 und 3 verwenden wir gulp und ein Compass-Plug-in, um scss in .css und .jade zu .html zu kompilieren, ähnlich wie bei 2 oben.

Einschließlich Polymerelementen

Um die Polymer-Elemente einzubeziehen, verwenden wir HTML-Importe.

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

Optimierung von Polymerelementen für die Produktion

Ein großes Projekt kann am Ende viele Polymer-Elemente haben. In unserem Projekt haben wir über 50. Wenn Sie davon ausgehen, dass jedes Element eine separate .js-Datei und einige Bibliotheken hat, auf die verwiesen wird, entstehen mehr als 100 separate Dateien. Das bedeutet, dass der Browser viele Anfragen stellen muss, mit Leistungseinbußen. Ähnlich wie bei einem Verkettungs- und Komprimierungsprozess, das wir auf einen Angular-Build anwenden würden, wird das Polymer-Projekt am Ende für die Produktion „vulkanisiert“.

Vulcanize ist ein Polymer-Tool, das die Abhängigkeitsstruktur in einer einzigen HTML-Datei vereinfacht und so die Anzahl der Anfragen reduziert. Dies ist besonders nützlich für Browser, die Webkomponenten nativ nicht unterstützen.

CSP (Content Security Policy) und Polymer

Wenn Sie sichere Webanwendungen entwickeln, müssen Sie die CSP implementieren. Die CSP ist eine Reihe von Regeln, die Cross-Site-Scripting-Angriffe (XSS) verhindern, also die Ausführung von Skripts aus unsicheren Quellen oder die Ausführung von Inline-Skripts aus HTML-Dateien.

Die optimierte, verkettete und komprimierte .html-Datei, die von Vulcanize generiert wurde, enthält nun den gesamten JavaScript-Code inline in einem nicht CSP-kompatiblen Format. Zur Behebung dieses Problems verwenden wir ein Tool namens Crisper.

Crisper teilt Inline-Skripts aus einer HTML-Datei auf und fügt sie zur Einhaltung der CSP in eine einzelne externe JavaScript-Datei ein. Wir übergeben die vulkanisierte HTML-Datei also über Crisper und erhalten zwei Dateien: elements.html und elements.js. Innerhalb von elements.html übernimmt er auch das Laden des generierten elements.js.

Logische Struktur der Anwendung

In Polymer können Elemente alles sein, von nicht visuellen Dienstprogrammen über kleine, eigenständige und wiederverwendbare UI-Elemente (z. B. Schaltflächen) bis hin zu größeren Modulen wie Seiten und sogar zum Erstellen vollständiger Anwendungen.

Eine logische Struktur der Anwendung auf oberster Ebene
Eine logische Struktur der Anwendung auf oberster Ebene, die durch Polymer-Elemente dargestellt wird.

Nachbearbeitung mit Polymer-Architektur und hierarchischer Architektur

In jeder 3D-Grafikpipeline gibt es immer einen letzten Schritt, bei dem Effekte als eine Art Overlay über dem Gesamtbild hinzugefügt werden. Dies ist der Nachbearbeitungsschritt. Er umfasst Effekte wie Schein, Gottstrahlen, Tiefenschärfe, Bokeh, Weichzeichner usw. Die Effekte werden je nach Aufbau der Szene kombiniert und auf verschiedene Elemente angewendet. In THREE.js könnten wir einen benutzerdefinierten Shader für die Nachbearbeitung in JavaScript erstellen oder wir können dies mit Polymer tun, dank seiner hierarchischen Struktur.

Sehen Sie sich den HTML-Code des Elements für das Postprozessor-Element an:

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

Die Effekte werden als verschachtelte Polymer-Elemente unter einer gemeinsamen Klasse angegeben. Dann tun wir in sw-experience-postprocessor.js Folgendes:

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

Wir verwenden die HTML-Funktion und querySelectorAll von JavaScript, um alle Effekte zu finden, die als HTML-Elemente im Postprozessor in der angegebenen Reihenfolge verschachtelt sind. Diese werden dann iteriert und dem Composer hinzugefügt.

Nehmen wir nun an, wir möchten den DOF-Effekt (Schärfentiefeneffekt) entfernen und die Reihenfolge der Blüten- und Vignettierungseffekte ändern. Wir müssen lediglich die Definition des Nachverarbeiters folgendermaßen ändern:

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

und die Szene läuft, ohne eine einzige Codezeile zu ändern.

Renderingschleife und Aktualisierungsschleife in Polymer

Mit Polymer können wir auch Rendering und Engine-Updates auf elegante Weise angehen. Wir haben ein timer-Element erstellt, das requestAnimationFrame verwendet und Werte wie die aktuelle Zeit (t) und die Deltazeit – die seit dem letzten Frame (dt) verstrichene Zeit, berechnet:

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

Anschließend verwenden wir die Datenbindung, um die Attribute t und dt an die Engine (experience.jade) zu binden:

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

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

Außerdem wird auf Änderungen von t und dt in der Engine gewartet. Wenn sich die Werte ändern, wird die _update-Funktion aufgerufen:

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

Wenn Sie sich jedoch für den FPS interessieren, können Sie die Datenbindung von Polymer in der Renderingschleife entfernen. So sparen Sie Millisekunden, die erforderlich sind, um Elemente über die Änderungen zu informieren. So implementierten wir benutzerdefinierte Beobachter:

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

Die Funktion addUpdateListener akzeptiert einen Callback und speichert ihn in ihrem Callback-Array. Dann iterieren wir in der Aktualisierungsschleife jeden Callback und führen ihn direkt mit den Argumenten dt und t aus, um Datenbindung oder das Auslösen von Ereignissen zu umgehen. Sobald ein Callback nicht mehr aktiv sein soll, haben wir die Funktion removeUpdateListener hinzugefügt, mit der Sie einen zuvor hinzugefügten Callback entfernen können.

Ein Lichtschwert in DREI.js

THREE.js abstrahiert die tiefgehenden Details von WebGL und ermöglicht es uns, uns auf das Problem zu konzentrieren. Unser Problem ist der Kampf gegen Sturmtruppen und wir brauchen eine Waffe. Bauen wir also ein Lichtschwert.

Die leuchtende Klinge unterscheidet ein Lichtschwert von jeder alten Zweihandwaffe. Sie besteht hauptsächlich aus zwei Teilen: dem Träger und dem Weg, der sichtbar wird, wenn er bewegt wird. Es hat eine helle Zylinderform und einen dynamischen Weg, der ihm folgt, während sich der Spieler bewegt.

Die Klinge

Die Klinge besteht aus zwei Teilklingen. Ein inneres und ein äußeres. Beide sind THREE.js-Mesh-Netzwerke mit ihren jeweiligen Materialien.

Innere Klinge

Für das innere Kling haben wir ein benutzerdefiniertes Material mit einem benutzerdefinierten Shader verwendet. Wir nehmen eine aus zwei Punkten erzeugte Linie und projizieren die Linie zwischen diesen beiden Punkten auf eine Ebene. Diese Ebene ist im Grunde das, was Sie steuern, wenn Sie mit Ihrem Mobilgerät kämpfen. Sie gibt dem Säbel das Gefühl von Tiefe und Ausrichtung.

Um das Gefühl eines runden leuchtenden Objekts zu erzeugen, betrachten wir die orhogonale Punktentfernung eines beliebigen Punkts auf der Ebene von der Hauptlinie, die die beiden Punkte A und B verbindet (siehe unten). Je näher ein Punkt an der Hauptachse liegt, desto heller ist er.

Inneres Blattlicht

Die folgende Quelle zeigt, wie ein vFactor berechnet wird, um die Intensität im Vertex-Shader zu steuern und dann zum Überblenden mit der Szene im Fragment-Shader zu verwenden.

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

};

Der äußere Klingenschein

Für den äußeren Schein rendern wir in einem separaten Renderingpuffer, verwenden einen Nachbearbeitungs-Bloom-Effekt und verschmelzen mit dem endgültigen Bild, um den gewünschten Schein zu erhalten. Die folgende Abbildung zeigt die drei verschiedenen Regionen, die du für einen angemessenen Säbel benötigst. Der weiße Kern, der mittlere blaue Schein und der äußere Schein.

Äußere Klinge

Lichtschwertweg

Die Spur des Lichtschwerts ist der Schlüssel zum vollen Effekt wie das Original aus der Star Wars-Serie. Der Weg wurde mit einem Fächer aus Dreiecken erstellt, das dynamisch anhand der Bewegung des Lichtschwerts erzeugt wurde. Diese Fans werden dann zur weiteren visuellen Verbesserung an den Nachprozessor weitergeleitet. Um die Lüftergeometrie zu erstellen, haben wir ein Liniensegment. Basierend auf seiner vorherigen Transformation und aktuellen Transformation generieren wir ein neues Dreieck im Netz, das nach einer bestimmten Länge vom Endstück abfällt.

Lichtschwertspur links
Lichtschwertspur rechts

Sobald wir ein Mesh-Netzwerk haben, weisen wir ihm ein einfaches Material zu und übergeben es an den Postprozessor, um einen reibungslosen Effekt zu erzielen. Wir verwenden denselben Bloom-Effekt, den wir auf das Schein auf der äußeren Klinge angewendet haben, und erhalten

Die komplette Strecke

Leuchten auf dem Weg

Damit der letzte Teil vollständig ist, mussten wir mit Schein um den tatsächlichen Weg herum umgehen, der auf verschiedene Arten erstellt werden konnte. Unsere Lösung, die wir hier nicht ins Detail gehen, bestand aus Leistungsgründen darin, einen benutzerdefinierten Shader für diesen Puffer zu erstellen, der eine glatte Kante um eine Klammer des Renderingpuffers erzeugt. Wir kombinieren diese Ausgabe dann im endgültigen Rendering. Hier sehen Sie den Schein, der den Pfad umgibt:

Weg mit Schein

Fazit

Polymer ist eine leistungsstarke Bibliothek und ein leistungsstarkes Konzept (wie WebComponents im Allgemeinen). Es liegt ganz bei dir, was du daraus machst. Das kann eine einfache UI-Schaltfläche oder eine WebGL-Anwendung in voller Größe sein. In den vorherigen Kapiteln haben wir Ihnen einige Tipps und Tricks gezeigt, wie Sie Polymer effizient in der Produktion einsetzen und komplexere Module strukturieren, die auch eine gute Leistung erzielen. Wir haben Ihnen auch gezeigt, wie Sie in WebGL ein schönes Lichtschwert erstellen. Wenn Sie all dies kombinieren, denken Sie daran, Ihre Polymer-Elemente vor der Bereitstellung auf dem Produktionsserver zu vulkanisieren. Falls Sie nicht vergessen, die Verwendung von Crisper zu verwenden, wenn Sie CSP-konform bleiben möchten, dann sind Sie vielleicht damit beschäftigt.

Gameplay