Tworzenie miecza świetlnego przy użyciu polimeru

Zrzut ekranu z mieczem świetlnym

Podsumowanie

Wykorzystaliśmy Polymer do stworzenia wydajnego, sterowanego mobilnym urządzeniem Lightsaber, który jest modułowy i konfigurowalny. Omawiamy najważniejsze szczegóły naszego projektu https://lightsaber.withgoogle.com/, aby pomóc Ci zaoszczędzić czas podczas tworzenia własnego projektu następnym razem, gdy natrafisz na gromadę wściekłych szturmowców.

Opis

Jeśli zastanawiasz się, co to jest Polymer lub WebKomponent, najlepiej zacząć od udostępnienia wyodrębnienia z rzeczywistego projektu. Oto próbka ze strony docelowej naszego projektu https://lightsaber.withgoogle.com. Jest to zwykły plik HTML, ale ma w sobie coś magicznego:

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

Obecnie istnieje wiele możliwości tworzenia aplikacji w formacie HTML5. Interfejsy API, platformy, biblioteki, silniki gier itp. Mimo wielu możliwości trudno jest stworzyć konfigurację, która łączy kontrolę nad wysoką wydajnością grafiki z przejrzystą strukturą modułową i skalowalnością. Okazało się, że Polymer pomaga nam utrzymać porządek w projekcie, a jednocześnie pozwala na optymalizowanie wydajności na niskim poziomie. Dlatego starannie podzieliliśmy projekt na komponenty, aby jak najlepiej wykorzystać jego możliwości.

Modułowość z użyciem polimeru

Polymer to biblioteka, która daje większą kontrolę nad sposobem tworzenia projektu na podstawie elementów niestandardowych wielokrotnego użytku. Umożliwia używanie samodzielnych, w pełni funkcjonalnych modułów zawartych w jednym pliku HTML. Obejmują nie tylko strukturę (znaczniki HTML), ale także wbudowane style i logikę.

Przeanalizuj ten przykład:

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

Przy większym projekcie warto podzielić te 3 komponenty logiczne (HTML, CSS i JS) i połączyć je tylko podczas kompilacji. Utworzyliśmy więc osobny folder dla każdego elementu w projekcie.

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

Folder każdego elementu ma taką samą strukturę wewnętrzną z osobnymi katalogami i plikami logicznymi (pliki kawy, style (pliki scss) i szablony (plik jade).

Oto przykładowy element sw-ui-logo:

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

A jeśli spojrzysz na plik .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')

Możesz zobaczyć uporządkowanie elementów, dodając style i logikę z oddzielnych plików. Aby umieścić nasze style w elementach Polymer, używamy instrukcji include Jade, więc po kompilacji mamy wbudowaną zawartość pliku CSS. Element skryptu sw-ui-logo.js jest uruchamiany w czasie działania.

Zależności modułowe z Bower

Zwykle biblioteki i inne zależności przechowujemy na poziomie projektu. Jednak w konfiguracji powyżej zobaczysz bower.json w folderze tego elementu: zależności na poziomie elementu. Oto podejście: w sytuacji, gdy masz wiele elementów o różnych zależnościach, możemy zadbać o to, by wczytać tylko te zależności, które zostały rzeczywiście wykorzystane. Gdy usuwasz element, nie musisz pamiętać o usuwaniu jego zależności, bo usuwasz też plik bower.json deklarujący te zależności. Każdy element niezależnie wczytuje związane z nim zależności.

Aby jednak uniknąć duplikowania zależności, w folderze każdego elementu umieszczamy też plik .bowerrc. To informuje, gdzie przechowywać zależności, dzięki czemu mamy pewność, że na końcu w tym samym katalogu jest tylko jedna z nich:

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

Dzięki temu, jeśli wiele elementów deklaruje THREE.js jako zależność, po zainstalowaniu jej dla pierwszego elementu i rozpoczęciu analizy drugiego elementu uświadamia sobie, że zależność jest już zainstalowana i nie pobiera jej ponownie ani nie powiela. Podobnie będzie w przypadku tych plików zależności, dopóki będzie zawierać co najmniej 1 element, który nadal określa go w jego bower.json.

Skrypt bash znajduje wszystkie pliki bower.json w strukturze elementów zagnieżdżonych. Następnie pojedynczo otwiera te katalogi i wykonuje polecenie bower install w każdym z nich:

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

Szablon szybkiego nowego elementu

Za każdym razem, gdy chcesz utworzyć nowy element, potrzeba trochę czasu: wygenerowanie folderu i podstawowej struktury plików z poprawnymi nazwami. Używamy więc języka Slush do napisania prostego generatora elementów.

Możesz wywołać skrypt z poziomu wiersza poleceń:

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

Zostanie utworzony nowy element, w tym cała struktura i zawartość pliku.

Zdefiniowaliśmy szablony plików elementów, np. szablon pliku .jade wygląda tak:

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

Generator Slush zastępuje zmienne rzeczywistymi ścieżkami i nazwami elementów.

Tworzenie elementów przy użyciu Gulp

Grzegorz zachowuje kontrolę nad procesem kompilacji. A w naszej strukturze elementy, dzięki którym Gulp powinien wykonać te czynności:

  1. Skompiluj pliki .coffee elementów do .js
  2. Skompiluj pliki .scss elementów do .css
  3. Skompiluj pliki .jade elementów w formacie .html, umieszczając w nim pliki .css.

Więcej informacji:

Kompiluję pliki .coffee elementów do .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')));
});

W krokach 2 i 3 używamy gulp i wtyczki kompasu, aby skompilować scss do .css oraz .jade do .html, podobnie jak w przypadku kroku 2.

Z pierwiastkami polimerowymi

Aby uwzględnić elementy Polymer, używamy importu 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">

Optymalizacja elementów Polymer na potrzeby produkcji

W przypadku dużego projektu może się okazać, że zawiera dużo pierwiastków Polymer. W naszym projekcie jest ich ponad 50. Jeśli weźmiesz pod uwagę, że każdy element ma oddzielny plik .js i niektóre biblioteki, do których odwołują się biblioteki, stanie się to ponad 100 osobnymi plikami. Oznacza to dużą liczbę żądań, które musi wysłać przeglądarka, jednocześnie zmniejsza wydajność. Podobnie jak w przypadku procesu konkatenacji i minimalizowania w przypadku kompilacji Angular, na końcu „wulkanizujemy” projekt Polymer.

Wulkanizuj to narzędzie Polymer, które spłaszcza drzewo zależności do jednego pliku HTML, co zmniejsza liczbę żądań. Jest to szczególnie przydatne w przypadku przeglądarek, które natywnie nie obsługują komponentów internetowych.

CSP (Content Security Policy) i Polymer

Podczas tworzenia bezpiecznych aplikacji internetowych musisz wdrożyć CSP. CSP to zestaw reguł, które uniemożliwiają prowadzenie ataków typu cross-site scripting (XSS), czyli wykonywanie skryptów z niebezpiecznych źródeł lub wykonywanie skryptów wbudowanych z plików HTML.

Ten, zoptymalizowany, połączony i zminifikowany plik .html wygenerowany przez Vulcanize zawiera cały kod JavaScript w formacie niezgodnym z CSP. Aby rozwiązać ten problem, używamy narzędzia Crisper.

Crisper dzieli skrypty wbudowane z pliku HTML i umieszcza je w jednym zewnętrznym pliku JavaScript, aby zapewnić zgodność z CSP. Dlatego przekazujemy wulkanizowany plik HTML przez narzędzie Crisper i w efekcie otrzymujemy 2 pliki: elements.html i elements.js. W obrębie elements.html zajmuje się też wczytywaniem wygenerowanego elements.js.

Logiczna struktura aplikacji

Elementy w Polymer to wszystko: od narzędzi niewizualnych przez małe, niezależne i wielokrotnego użytku elementy interfejsu (np. przyciski) po większe moduły, takie jak „strony”, a nawet tworzenie pełnych aplikacji.

Logiczna struktura najwyższego poziomu aplikacji
Logiczna struktura najwyższego poziomu naszej aplikacji reprezentowana przez elementy Polymer.

Przetwarzanie końcowe z wykorzystaniem technologii Polymer i architektury nadrzędnej i podrzędnej

W każdym potoku grafiki 3D zawsze występuje ostatni krok, w którym efekty są dodawane do całego zdjęcia jako rodzaj nakładki. To etap przetwarzania, który obejmuje efekty takie jak poświaty, promienie bokeh, głębię pola, bokeh, rozmycia itp. Efekty są łączone i zastosowane do różnych elementów w zależności od tego, jak powstała scena. W kodzie THREE.js możemy utworzyć niestandardowy program do cieniowania do późniejszego przetwarzania w JavaScripcie albo w Polymer dzięki strukturze nadrzędny-podrzędny.

Jeśli spojrzysz na kod HTML elementu naszego procesora:

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

Efekty określamy jako zagnieżdżone elementy Polymer w ramach wspólnej klasy. Następnie w sw-experience-postprocessor.js:

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

Używamy funkcji HTML i funkcji querySelectorAll w języku JavaScript, aby znaleźć wszystkie efekty zagnieżdżone jako elementy HTML w obrębie procesora postów, w kolejności, w jakiej zostały określone. Następnie powtarzamy je i dodajemy do narzędzia composer.

Załóżmy teraz, że chcemy usunąć efekt DOF (głębokość pola) oraz zmienić kolejność efektów kwitnienia i winietowania. Wystarczy zmienić definicję osoby przetwarzającej na taką:

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

Cała scena będzie po prostu odtwarzana, nie zmieniając ani jednego wiersza rzeczywistego kodu.

Pętla renderowania i pętla aktualizacji w technologii Polymer

Polymer pozwala też elegancko podchodzić do renderowania i aktualizowania silników. Utworzyliśmy element timer, który używa parametru requestAnimationFrame i oblicza wartości takie jak bieżący czas (t) i czas delta – czas, który upłynął od ostatniej klatki (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()

Następnie używamy wiązania danych, aby powiązać właściwości t i dt z naszą wyszukiwarką (experience.jade):

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

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

Nasłuchujemy też w wyszukiwarce zmian wartości t i dt. Gdy te wartości się zmienią, funkcja _update zostanie wywołana:

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

Jeśli jednak zależy Ci na szybkości FPS, możesz usunąć powiązanie danych Polymer w pętli renderowania, aby zaoszczędzić kilka milisekund wymaganych do powiadamiania elementów o zmianach. Wprowadziliśmy takie narzędzia:

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

Funkcja addUpdateListener akceptuje wywołanie zwrotne i zapisuje je w swojej tablicy wywołań zwrotnych. Następnie w pętli aktualizacji powtarzamy każde wywołanie zwrotne i realizujemy je bezpośrednio z argumentami dt i t, omijając wiązanie danych lub uruchamianie zdarzeń. Gdy wywołanie zwrotne przestanie być aktywne, dodaliśmy funkcję removeUpdateListener, która umożliwia usunięcie dodanego wcześniej wywołania zwrotnego.

Miecz świetlny w THREE.js

THREE.js wydobywa niskopoziomową szczegółowość WebGL i umożliwia nam skupienie się na problemie. Nasz problem polega na walce ze szturmowcami, a potrzebujemy broni. Zbudujmy więc miecz świetlny.

Błyszczący ostrze to coś, co odróżnia miecz świetlny od każdej starej broni dwuręcznej. Składa się głównie z dwóch części: belki i szlaku, który widać podczas przenoszenia. Ma jasny kształt walca i dynamiczny ślad, który przemieszcza się wraz z ruchem gracza.

Ostrze

Ostrze składa się z dwóch łopatek podrzędnych. Wewnętrzne i zewnętrzne. Obie sieci są siatkami THREE.js z odpowiednimi materiałami.

Ostrze wewnętrzne

Wewnętrzne ostrze zastosowaliśmy z niestandardowego materiału z niestandardowym cieniem. Wybieramy linię utworzoną przez dwa punkty i rzutujemy ją na płaszczyznę. To samolot, którym sterujesz za pomocą telefonu komórkowego. Daje on poczucie głębi i orientacji miecza.

Aby stworzyć wrażenie okrągłego, świecącego obiektu, sprawdzamy odległość punktu ortogonalnego dowolnego punktu na płaszczyźnie od linii głównej łączącej dwa punkty A i B jak poniżej. Im punkt bliżej osi głównej, tym bardziej głęboko.

Poświata wewnętrznego ostrza

Źródło poniżej pokazuje, jak obliczamy obiekt vFactor, aby kontrolować intensywność w cieniowaniu wierzchołków, a następnie używać go do mieszania się ze sceną w cieniu fragmentów.

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

};

Zewnętrzne ostrze

W przypadku blasku zewnętrznego renderujemy go w osobnym buforze renderowania. Wykorzystujemy efekt rozkwitu po przetworzeniu i mieszamy z ostatecznym obrazem, aby uzyskać pożądaną poświatę. Na ilustracji poniżej widać 3 różne regiony, które będą Ci potrzebne, jeśli potrzebujesz porządnej szabry. To białe rdzeń: środkowa, niebieski, poświata i poświata zewnętrzna.

Ostrze zewnętrzne

Szlak z mieczem świetlnym

Ścieżka miecza świetlnego jest kluczem do osiągnięcia pełnego efektu, jak w przypadku pierwowzorów z serii Gwiezdne Wojny. Szlak przebyliśmy wachlarzem trójkątów, generowanym dynamicznie ruchem miecza świetlnego. Wentylatory te są następnie przekazywane do zespołu przetwarzającego w celu dalszego uszczegółowienia obrazu. Aby utworzyć geometrię wentylatora, mamy segment linii. Na podstawie poprzedniej przekształcenia oraz przekształcenia bieżącego generujemy w siatce nowy trójkąt, który po pewnej długości wypuszcza część ogona.

ślad miecza świetlnego w lewo.
Szlak miecza świetlnego w prawo

Po utworzeniu sieci typu mesh przypisujemy do niej prosty materiał i przekazujemy go do podmiotu przetwarzającego, aby uzyskać płynny efekt. Używamy tego samego efektu rozkwitu dla poświaty zewnętrznego łopatki, aby uzyskać gładki ślad, jak widać:

Pełna trasa

Poświata na szlaku

Ukończenie finalnej części prac wymagało użycia poświaty wokół faktycznego szlaku, który można było tworzyć na wiele sposobów. Rozwiązaniem, którym nie szczegółowo omawiamy, ze względu na wydajność było utworzenie dla tego bufora niestandardowego kształtu, który tworzy gładką krawędź wokół bufora renderowania. Później łączymy uzyskane dane w końcowym renderowaniu, więc widać poświatę otaczającą szlak:

Szlak ze blaskiem

Podsumowanie

Polymer to zaawansowana biblioteka i koncepcja (tak samo jak w ogóle WebKomponenty). Tylko od Ciebie zależy, co z nimi zrobi. Może to być wszystko, od prostego przycisku interfejsu po pełnowymiarową aplikację WebGL. W poprzednich rozdziałach przedstawiliśmy kilka wskazówek i porad dotyczących efektywnego wykorzystania Polymer w produkcji oraz tworzenia struktury bardziej złożonych modułów, które również dobrze się sprawdzają. Pokazaliśmy też, jak stworzyć ładnie wyglądający miecz świetlny w WebGL. Jeśli więc połączysz to wszystko, pamiętaj o wulkanizacji elementów Polymer przed wdrożeniem ich na serwerze produkcyjnym. Jeśli nie zapomnisz też korzystać z Crispera, jeśli chcesz zachować zgodność z CSP, być może moc jest z Tobą.

Rozgrywka