Lulucat

Dlaczego zastąpiliśmy trzy silniki geometrii zakreślacza stemplami

Gaoge ZhangGaoge Zhang

Dane z Apple Pencil ujawniły wady pominięte przez testy syntetyczne, prowadząc Lulucat Notes od trzech silników konturu do modelu stempli MaLiang.

Zaktualizowano

Lulucat Notes zaczynał od konwencjonalnego renderera zakreślacza: budował geometryczny kontur wokół próbkowanej ścieżki, a następnie wypełniał ten kontur raz półprzezroczystym kolorem. Pojedyncze wypełnienie zapobiega ciemniejszym pasom pojawiającym się tam, gdzie półprzezroczyste segmenty nachodzą na siebie. Umieszcza też każde połączenie, samoprzecięcie, zakończenie i krótkie pociągnięcie w ramach tego samego problemu geometrycznego.

Trzy generacje własnej geometrii usuwały po jednej klasie wad, ujawniając kolejną. Obecnie używany renderer wykorzystuje miękkie okrągłe stemple oparte na MaLiang, objętym licencją MIT frameworku open source do rysowania autorstwa Harley-xk. W zamian za usunięcie całej kategorii awarii konturu zaakceptowaliśmy ciemniejsze samoprzecięcia i zakończenia zwężane przez nacisk.

Niebieskie pismo zakreślaczem zarejestrowane na iPadzie, z cienkimi haczykami widocznymi na początku i końcu kilku pociągnięć.

Oryginalny zrzut ekranu z urządzenia przy powiększeniu 228%. Na obu końcach kilku pociągnięć pojawiają się małe haczyki.

Rzeczywiste pismo tworzyło haczyki, których nie było w scenach syntetycznych

Nasze sceny syntetyczne obejmowały linie proste, łuki, krótkie dotknięcia, ruchy powrotne, zmiany nacisku i ostre skręty — wszystkie renderowały się poprawnie. Rzeczywiste chińskie pismo nadal tworzyło na początku i końcu wielu pociągnięć haczyki o długości około 1–3 punktów.

Dlatego dodaliśmy do aplikacji przycisk zapisu i zarejestrowaliśmy 29 pełnych pociągnięć. Każda próbka zachowała pozycję, promień, nacisk, azymut, kąt wysokości, znacznik czasu i flagi estymacji UIKit. Program offline zdekodował ten JSON i skompilował te same pliki Stroke.swift, StrokeRenderer.swift oraz HighlighterStrokeBuilder.swift, których używa aplikacja. Jego wynik odpowiadał zrzutowi ekranu z urządzenia pociągnięcie po pociągnięciu, łącznie z haczykami.

Ponieważ renderer offline odtworzył wadę wyłącznie na podstawie zarejestrowanych danych wejściowych, jej przyczyna leżała w próbkach i geometrii, a nie w zachowaniu wyświetlania. Mogliśmy teraz odczytać dokładne próbki, które trafiły do renderera.

Nasza pierwsza hipoteza obwiniała estymowaną orientację Pencila przy dotknięciu i uniesieniu. Dane ją odrzuciły. We wszystkich 29 pociągnięciach azymut zmieniał się najwyżej o 0.09 radiana, a kąt wysokości najwyżej o 0.02 radiana w obrębie pierwszych i ostatnich ośmiu próbek, a każda flaga estymacji na tym iPadzie miała wartość false. Orientacja pozostawała stabilna, podczas gdy nacisk i kierunek zmieniały się gwałtownie.

Próg nacisku wynikał z czystej przerwy w danych

Ostatnia próbka każdego zarejestrowanego pociągnięcia miała nacisk 0.000. Próbki podczas pisania miały wartość co najmniej 0.01, natomiast próbki zarejestrowane po oderwaniu Pencila od powierzchni miały wartość co najwyżej 0.002. Te próbki z rysikiem w powietrzu często trwały jeszcze przez 2–7 punktów i przemieszczały się o 0.8–9.0 punktów, a ich kierunek mógł odchylić się o około 90 stopni od kierunku pisanego pociągnięcia.

Pociągnięcie 2 pokazuje ten mechanizm. Jego korpus kończył się między 143 a 180 stopniami. Ogon o niskim nacisku skręcał następnie od −90 do −67 stopni i przemieszczał się o kolejne 8.5 punktu. Renderer konturu traktował ostatnią próbkę z rysikiem w powietrzu jako właściwy punkt końcowy, więc rozciągał wąski fragment geometrii w jej stronę.

Zmierzony nacisk zostawia niemal rząd wielkości różnicy między ruchem podczas pisania a ruchem w powietrzu, dlatego umieściliśmy próg wewnątrz tej przerwy:

private static let pressureFloor: CGFloat = 0.005

Usuwane są tylko próbki o niskim nacisku na obu końcach. Próbka o niskim nacisku w środku pociągnięcia pozostaje jego częścią. Reguła odpowiada zmierzonej granicy zdarzenia i pozostawia pisaną ścieżkę bez zmian.

Instrumentowane zbliżenie przed przycięciem: zielone próbki wejściowe odchylają się od pociągnięcia, podczas gdy czerwony kontur tworzy wąski haczyk.

Przed przycięciem renderer zakotwicza zakończenie w próbkach z rysikiem w powietrzu. Zielone kropki to surowe próbki; czerwona linia to wygenerowany kontur.

Instrumentowane zbliżenie po przycięciu: niebieskie wypełnienie kończy się na ostatnich próbkach podczas pisania, a zielone kropki o niskim nacisku pozostają poza nim.

Po przycięciu próbek poniżej 0.005 zakończenie kończy się w ostatnim zapisanym punkcie. Ten próg nacisku pozostaje w obecnym rendererze stempli.

Próg usunął długie haczyki tworzone przez ruch w powietrzu. Krótkie przeciągnięcia przy dotknięciu i łuki przy unoszeniu, których nacisk pozostawał powyżej 0.005, przetrwały, ponieważ są ruchem pisanym. Obsługa tych ruchów w modelu konturu doprowadziła do kolejnych rund prac nad geometrią.

Trzy silniki geometrii przesuwały wady

Generacja pierwsza: jeden kontur, jedno wypełnienie

Pierwszy silnik wrażliwy na nacisk stosował znajomy potok. Wygładzał oś, obliczał lewe i prawe przesunięcia, łączył je w jeden wielokąt i raz wypełniał ten wielokąt. Końcówka była elipsą sterowaną azymutem i kątem wysokości Apple Pencila, więc jej przekrój zmieniał się wraz z kierunkiem pisania. Krótkie dotknięcia miały osobny kształt, a płaskie zakończenia wykorzystywały niewielkie asymetrie, aby uniknąć obszarów o zerowej liczbie owinięć.

Pojedyncze wypełnienie rozwiązywało problem kumulacji krycia wewnątrz pociągnięcia i wymagało poprawnego wielokąta przy każdym skręcie. Przeciągnięcia przy dotknięciu tworzyły kliny między zakończeniem a korpusem. Ruchy powrotne mogły tworzyć białe sierpowate obszary. Ostre skręty mogły powodować samoprzecięcie wewnętrznego konturu i pozostawiać niewypełnione wcięcie.

Generacja druga: naprawa konturu przy końcach

Drugi silnik zachował kontur i dodał lokalne naprawy: przycinanie ruchów powrotnych, tłumienie wczesnych estymowanych właściwości, prostowanie punktów końcowych, przycinanie przejściowego kierunku i niewielkie wydłużenie zakończenia. Każda poprawka usuwała odtworzoną awarię. Razem sprawiły, że zachowanie przy końcach zależało od kilku progów i okien.

Rzeczywiste pisanie tworzyło następnie przypadki wykraczające poza te okna. Stałe okno prostowania o długości 24 punktów usuwało zamierzone haczyki w krótkich pociągnięciach. Okno adaptacyjne zachowywało te haczyki, a łuki przy unoszeniu nadal mogły zamieniać się w widoczne narożniki. Przycinanie uwzględniające kierunek usuwało więcej przejściowego ruchu, kosztem odrzucenia około 2.5 punktu rzeczywiście zakrzywionego początku w jednym zarejestrowanym pociągnięciu.

Każda poprawka naprawiała swój przypadek docelowy. Model pod nimi nadal wymagał twardego konturu, który pozostawał poprawny przy zaszumionych, zakrzywionych danych wejściowych.

Generacja trzecia: suma pokrycia z przyciętymi kwadratowymi zakończeniami

Trzeci silnik przestał konstruować pojedynczy kontur. Rysował trapez dla każdego segmentu i dysk w każdej wewnętrznej próbce na masce w odcieniach szarości, wyznaczał sumę ich pokrycia, a następnie raz nakładał kolor zakreślacza. Sierpowate obszary przy ruchach powrotnych, przyciemnienie samoprzecięć i dziury o zerowej liczbie owinięć znikały, ponieważ renderer nie zależał już od kierunku obiegu wielokąta.

Suma zaokrąglała końce, więc przywróciliśmy kwadratowe zakończenia, przycinając pokrycie względem dwóch półpłaszczyzn końcowych. To przycięcie naprawiło proste linie o stałym nacisku i wprowadziło poważniejszą wadę: korpus zakrzywionego pociągnięcia może prawidłowo przecinać półpłaszczyznę końcową, a przycięcie zakończenia usuwało wtedy część środka pociągnięcia. Podczas wprowadzania na żywo kierunek końca ciągle się zmieniał, więc przycinany obszar przesuwał się, a pociągnięcie migotało.

Zakrzywione niebieskie pociągnięcie przed poprawką przycięcia zakończenia, z górnym korpusem odciętym wzdłuż ukośnej linii; zielone kropki pokazują całą próbkowaną ścieżkę.

Globalne ograniczenie zakończenia przecina korpus zakrzywionego pociągnięcia.

To samo zakrzywione niebieskie pociągnięcie po dodaniu lokalnej bramki długości łuku, z obiema gałęziami przywróconymi wokół zielonej próbkowanej ścieżki.

Ograniczenie przycinania zakończenia do prymitywów znajdujących się w odległości jednego półszerokości od punktu końcowego przywraca to pociągnięcie.

Bramka długości łuku uczyniła przycinanie zakończenia lokalnym i naprawiła każdy zarejestrowany przypadek. Skrajny syntetyczny haczyk nadal pokazywał małe białe wcięcie w miejscu, gdzie końcówka o niskim nacisku stykała się z przyciętym zakończeniem. Trzeci silnik znów wymienił jedną awarię geometrii na inną.

MaLiang usuwa twardą granicę

MaLiang buduje pociągnięcie z powtarzanych stempli tekstury zamiast z jednego zamkniętego konturu. Jego generator ścieżki wykorzystuje kwadratowe segmenty Béziera przechodzące przez środki sąsiednich próbek, a renderer linii umieszcza stemple w regularnym odstępie wzdłuż tej ścieżki. Rozmiar stempla, obrót, kolor i krycie mogą zmieniać się wzdłuż pociągnięcia.

Miękki stempel nie ma osobnego zakończenia, połączenia ani reguły obiegu. Ostra zmiana kierunku pozostaje sekwencją nachodzących na siebie znaków. Ruchy powrotne nie mogą tworzyć pustej kieszeni w wielokącie, a półpłaszczyzna końcowa nie może przeciąć środka pociągnięcia, ponieważ model nie zawiera przycięcia względem punktu końcowego.

Przyjęliśmy model, a nie kod. Obecny HighlighterStrokeBuilder wykonuje cztery operacje:

  1. Usuwa tylko końcowe próbki, których nacisk jest niższy niż 0.005.
  2. Wygładza pozycje za pomocą kwadratowych segmentów Béziera przechodzących przez punkty środkowe.
  3. Umieszcza po jednym okrągłym stemplu na każdy punkt długości łuku.
  4. Ustala średnicę stempla na podstawie znormalizowanego nacisku i nakłada stemple z alfą source-over.

Builder zmniejszył się z 686 linii do 190. Azymut, kąt wysokości, konstrukcja konturu, maski sumy, okna kierunku końca, wydłużenie zakończenia i przycinanie zakończenia zniknęły ze ścieżki zakreślacza. Nadal rejestrujemy orientację Pencila, ponieważ model wejściowy ją obsługuje, ale sam zakreślacz nie odczytuje już tych pól.

Wzór nacisku MaLiang wymagał kalibracji urządzenia

MaLiang ustala rozmiar stempla za pomocą wzoru równoważnego

który zakłada, że force wykorzystuje użyteczną część zakresu od 0 do 1. Nasze dane z urządzenia tego nie potwierdziły. W około 3 400 próbkach Apple Pencil mediana nacisku wynosiła 0.047, a maksimum 0.178. Bezpośrednie podanie tych wartości do wzoru sprawiało, że zwykłe pismo było znacznie cieńsze niż nominalny rozmiar pędzla.

Dlatego normalizujemy nacisk względem zaobserwowanej mediany, następnie stosujemy wykładnik i ograniczamy wynik:

Mediana nacisku podczas pisania odpowiada teraz nominalnej średnicy 12 punktów. Bardzo lekki kontakt pozostaje widoczny przy 35% nominalnego rozmiaru, a silny nacisk przestaje zwiększać rozmiar po osiągnięciu 120%.

Ta kalibracja należy do urządzenia wejściowego i zachowania pędzla, a nie do MaLiang. Skopiowanie oryginalnego wzoru bez zmierzenia własnego rozkładu nacisku zachowałoby architekturę, ale dałoby niewłaściwy pędzel.

Alfa pojedynczego stempla wynikała ze znanej liczby nakładających się warstw

MaLiang kompensuje nakładające się półprzezroczyste stemple za pomocą empirycznego wyrażenia alpha ÷ overlapping × 2.5. Nasz pędzel ma stałą nominalną średnicę 12 punktów i odstęp między stemplami wynoszący 1 punkt, więc jego oś otrzymuje około 12 warstw. W naszym przypadku liczba nakładających się warstw jest znana, dlatego kompensację można obliczyć zamiast szacować.

Dla docelowego krycia pociągnięcia i równych warstw source-over stempel o kryciu daje

więc jeden stempel wymaga

Dla i każdy stempel używa krycia około . Środek osiąga zamierzony kolor po wielokrotnym nakładaniu, podczas gdy krawędź otrzymuje mniej stempli i zachowuje jednopunktowe lub dwupunktowe, łagodnie zanikające przejście.

Arkusz różnorodnych niebieskich pociągnięć zakreślacza wyrenderowanych przez końcowy model miękkich stempli, obejmujący pętle, haczyki, krótkie znaki i samoprzecięcia.

Końcowy renderer odtwarzający zarejestrowane ścieżki z urządzenia. Brak haczyków, ukośnych cięć i białych pustych obszarów.

Dwa koszty, które zaakceptowaliśmy

Samoprzecięcia stają się ciemniejsze. Kompozycja source-over liczy każdy stempel, w tym stemple położone podczas wcześniejszego przejścia przez ten sam obszar, a silnik sumy pokrycia tego unikał. Zaakceptowaliśmy ciemniejsze nakładanie, ponieważ jest przewidywalne i zachowuje kształt pociągnięcia.

Końce zwężają się wraz z naciskiem. Rzeczywiste uniesienie Pencila zmniejsza nacisk, co zmniejsza średnicę stempla, więc pociągnięcie zwęża się zamiast kończyć pełnej szerokości kwadratowym zakończeniem. Przez wszystkie trzy silniki geometrii traktowaliśmy to kwadratowe zakończenie jako wymaganie. Porzucenie go usunęło przycinanie i mechanikę końców stojącą za kilkoma opisanymi wyżej awariami, więc zaakceptowaliśmy zwężony koniec.

Model, który dostarczamy, robi zatem mniej niż poprzednie. Zachowuje zmierzone zdarzenie pisania, reaguje na nacisk po kalibracji urządzenia i renderuje zakrzywione oraz samoprzecinające się ścieżki bez wymagania globalnie poprawnego konturu. Końcówka w kształcie dłuta, idealnie kwadratowe zakończenie i dokładna polilinia między surowymi próbkami to cechy, z których zrezygnowaliśmy w zamian za ten efekt.

Próg nacisku można najłatwiej przenieść do innych projektów. Wartość 0.005 wynikała z przerwy między dwiema grupami w rzeczywistych danych wejściowych: pisaniem przy 0.01 i powyżej oraz śledzeniem w powietrzu przy 0.002 i poniżej. Te same zarejestrowane pociągnięcia pokazały, gdzie zawiodła każda naprawa geometrii. Te awarie doprowadziły do modelu stempli i określiły dwa koszty, które on niesie.