W przypadku obszaru roboczego na Macu, który obejmuje ogromne współrzędne i drobne szczegóły, przechowuj położenia zgrubne oddzielnie od lokalnych przesunięć. Odejmij początek widoku przed zastosowaniem powiększenia i sprawdzaj precyzję przy każdej konwersji. W tym samouczku obliczenia są wykonywane na danych syntetycznej sceny.

Konkretne zadanie polega na umieszczeniu syntetycznego znacznika na powiększalnym obszarze roboczym na komputerze, bez utraty jego niewielkiego przesunięcia z powodu dużego adresu w scenie. Przykład dotyczy jednej osi; te same obliczenia zastosuj niezależnie do drugiej osi. Wynikiem są liczbowe współrzędne rysowania, dzięki czemu możesz sprawdzić rachunki przed podłączeniem ich do obszaru roboczego.

Dlaczego niewielkie przesunięcie znika przy dużej współrzędnej?

Typ Double w języku Swift jest 64-bitowym typem zmiennoprzecinkowym. Jego zakres obejmuje duże i małe wartości, ale nie oznacza to, że może zachować każde niewielkie przyrostowe przesunięcie dodane do dużej wartości. Apple opisuje ten typ i jego interfejsy konwersji w dokumentacji Double.

Liczby zmiennoprzecinkowe o podwójnej precyzji w systemie binarnym mają 53 znaczące cyfry binarne. Dlatego odstępy między reprezentowalnymi wartościami rosną wraz z ich wielkością. W opracowaniu Goldberga na temat arytmetyki wyjaśniono zarówno tę reprezentację, jak i to, w jaki sposób odejmowanie wcześniej zaokrąglonych operandów może ujawnić utratę informacji, która nastąpiła wcześniej. Co każdy informatyk powinien wiedzieć o arytmetyce zmiennoprzecinkowej

Rozważmy te jawnie poglądowe dane wejściowe: indeks komórki 2^40, szerokość komórki 1024 jednostek sceny i przesunięcie znacznika 2^-30 jednostek sceny. Połączenie adresu komórki z jej szerokością daje współrzędną bezwzględną 2^50. Przy tej dodatniej współrzędnej sąsiednie wartości Double dzieli 0.25 jednostek. Dodanie niewielkiego przesunięcia zaokrągla wynik z powrotem do pierwotnej współrzędnej.

Późniejsze odjęcie początku daje więc zero. Pomnożenie tego zera przez duży współczynnik powiększenia nie pozwoli odzyskać przesunięcia. Utrata nastąpiła podczas składania współrzędnej bezwzględnej.

Użyj x.ulp, aby sprawdzić odstępy przy wielkości, którą faktycznie przechowujesz. Apple definiuje tę wartość jako jednostkę ostatniego miejsca i opisuje jej wyjątki, w tym wartości niefinitywne. Double.ulpOfOne określa odstęp przy jedynce; nie jest uniwersalną tolerancją bezwzględną dla współrzędnych sceny. ulp

Co powinna zachowywać reprezentacja współrzędnych?

Na potrzeby tego obszaru roboczego każdą oś reprezentuj za pomocą indeksu komórki Int64 i lokalnego przesunięcia Double. Położenie ma pojęciowo postać komórka × rozmiarKomórki + lokalne, ale obliczenia powinny unikać tworzenia tej bezwzględnej wartości zmiennoprzecinkowej.

Od początku przechowuj znacznik i początek widoku w tej rozdzielonej postaci. Konwersja zaokrąglonej już współrzędnej bezwzględnej na komórkę i przesunięcie nie odtworzy utraconych szczegółów.

Poniższe założenia projektowe dotyczą opracowanego przykładu; nie są uniwersalnymi limitami liczbowymi:

  • Każda komórka ma dokładnie 1024 jednostek sceny szerokości.
  • Indeksy komórek mają typ Int64; to adresy lub liczniki, a nie wielkości zmiennoprzecinkowe.
  • Każde lokalne przesunięcie jest skończone i mieści się w przedziale 0..<1024.
  • Dokładne pozycjonowanie dopuszcza wyłącznie komórkę początku i jej bezpośrednich sąsiadów.
  • Powiększenie jest skończoną, dodatnią liczbą jednostek wyświetlania na jednostkę sceny.
  • Początek ekranu to syntetyczna współrzędna rysowania 512.

Oblicz współrzędną względną jako różnicaIndeksówKomórek × rozmiarKomórki + różnicaLokalna. Co kluczowe, odejmij indeksy komórek jako liczby całkowite przed konwersją różnicy na Double. Zwykła konwersja liczb całkowitych w Apple może zaokrąglać; Double(exactly:) zamiast tego kończy się niepowodzeniem, gdy dokładna reprezentacja jest niemożliwa. Double

Nawet odejmowanie dwóch prawidłowych wartości Int64 może dać wynik przekraczający zakres tego typu. Użyj subtractingReportingOverflow(_:) i sprawdź jego flagę przed użyciem wyniku. Apple wyraźnie dokumentuje, że w przypadku przepełnienia wynik częściowy jest obcinany. subtractingReportingOverflow(_:)

Taki układ zachowuje duży adres niezależnie od lokalnych szczegółów. Precyzja nadal zależy od lokalnych przesunięć: dodanie bardzo małego przyrostu do wystarczająco dużego lokalnego przesunięcia może go zgubić. Dobierz szerokość komórek i lokalne punkty odniesienia do najdrobniejszego szczegółu potrzebnego w bieżącym widoku.

Wykonaj pełne lokalne obliczenia na Macu

Potrzebujesz działającego łańcucha narzędzi Swift. W razie potrzeby postępuj zgodnie z aktualnymi instrukcjami Instalowanie Swift — macOS. W aplikacji Terminal sprawdź instalację i utwórz mały pakiet wykonywalny, korzystając z procedury opisanej w Tworzenie narzędzia wiersza poleceń.

swift --version
mkdir SceneCoordinates
cd SceneCoordinates
swift package init --name SceneCoordinates --type executable

Wygenerowanym pliku Swift będącym punktem wejścia w celu wykonywalnym, w katalogu Sources, zastąp jego zawartość kompletnym programem poniżej. Umieść ten program w jednym pliku punktu wejścia, bez dodatkowych instrukcji najwyższego poziomu. Importuje on tylko bibliotekę standardową Swift i nie wymaga zależności pakietów.

import Swift

struct AxisPosition {
    let cell: Int64
    let local: Double
}

enum CoordinateError: Error {
    case invalidInput
    case cellDifferenceOverflow
    case outsideDetailedView
    case inexactCellConversion
    case unrepresentableResult
}

func screenCoordinate(
    point: AxisPosition,
    origin: AxisPosition,
    zoom: Double
) throws -> Double {
    let cellSize = 1024.0
    let screenOrigin = 512.0

    guard point.local.isFinite,
          origin.local.isFinite,
          point.local >= 0, point.local < cellSize,
          origin.local >= 0, origin.local < cellSize,
          zoom.isFinite, zoom > 0 else {
        throw CoordinateError.invalidInput
    }

    let difference = point.cell.subtractingReportingOverflow(origin.cell)
    guard !difference.overflow else {
        throw CoordinateError.cellDifferenceOverflow
    }
    guard (-1...1).contains(difference.partialValue) else {
        throw CoordinateError.outsideDetailedView
    }
    guard let cells = Double(exactly: difference.partialValue) else {
        throw CoordinateError.inexactCellConversion
    }

    let localDifference = point.local - origin.local
    let delta = localDifference.addingProduct(cells, cellSize)
    let scaled = delta * zoom
    guard delta.isFinite, scaled.isFinite,
          delta == 0 || scaled != 0 else {
        throw CoordinateError.unrepresentableResult
    }

    let screen = screenOrigin.addingProduct(delta, zoom)
    guard screen.isFinite,
          delta == 0 || screen != screenOrigin else {
        throw CoordinateError.unrepresentableResult
    }
    return screen
}

@main
struct SceneCoordinates {
    static func main() throws {
        let cell: Int64 = 1_099_511_627_776
        let step = Double(sign: .plus, exponent: -30, significand: 1)
        let zoom = Double(sign: .plus, exponent: 30, significand: 1)
        let absoluteBase = Double(cell) * 1024.0
        let collapsed = (absoluteBase + step) - absoluteBase

        let screen = try screenCoordinate(
            point: AxisPosition(cell: cell, local: step),
            origin: AxisPosition(cell: cell, local: 0),
            zoom: zoom
        )

        let tiny = Double(sign: .plus, exponent: -600, significand: 1)
        let largeZoom = Double(sign: .plus, exponent: 600, significand: 1)
        let tinyScreen = try screenCoordinate(
            point: AxisPosition(cell: 0, local: tiny),
            origin: AxisPosition(cell: 0, local: 0),
            zoom: largeZoom
        )

        precondition(absoluteBase.ulp == 0.25)
        precondition(collapsed == 0)
        precondition(screen == 513)
        precondition(tinyScreen == 513)

        print("Absolute spacing:", absoluteBase.ulp)
        print("Absolute displacement:", collapsed)
        print("Split-coordinate screen:", screen)
        print("Tiny-coordinate screen:", tinyScreen)
    }
}

Uruchom pakiet z jego katalogu:

swift run SceneCoordinates

Oczekiwane wyniki liczbowe to kolejno 0.25, 0.0, 513.0 i 513.0. Są to wyniki obliczeń dla podanych danych syntetycznych. Odstępy binary64, zanik przesunięcia i skalowanie przez potęgi dwójki sprawdzono w środowisku badawczym za pomocą Pythona; nie było tam dostępnego kompilatora Swift, więc program w Swift przeanalizowano, ale go nie uruchomiono.

Pierwszy stabilny wynik zachowuje 2^-30 oddzielnie od ogromnego adresu komórki, a następnie skaluje go przez 2^30, uzyskując jedną jednostkę wyświetlania. Drugi używa 2^-600 i powiększenia 2^600; obie wartości wejściowe i ich iloczyn są w tym przykładzie reprezentowalne. Mała wartość sama w sobie nie oznacza utraty precyzji.

Program używa a.addingProduct(b, c) dla wyrażenia a + b × c. Apple opisuje je jako operację łączoną, w której nie dochodzi do zaokrąglenia pośredniego. Ogranicza to zaokrąglenia w ramach tej operacji; lokalne odejmowanie i wcześniejsze zaokrąglenia danych wejściowych pozostają osobnymi etapami. addingProduct(::)

Jak dobrać użyteczny budżet precyzji?

Zacznij od najmniejszego odstępu, który musi pozostać widoczny przy bieżącym powiększeniu. Dla poglądowego celu wynoszącego jedną czwartą jednostki wyświetlania podziel 0.25 przez powiększenie, aby uzyskać budżet błędu w przestrzeni sceny. Następnie sprawdź odstępy przy lokalnych współrzędnych i wartościach pośrednich używanych w obliczeniach.

Na przykład przy powiększeniu 2^30 poglądowy budżet wynosi 2^-32 jednostek sceny. Przy lokalnej współrzędnej 512 odstęp typu Double wynosi 2^-43, czyli jest znacznie mniejszy od tego budżetu. Przy współrzędnej bezwzględnej 2^50 odstęp wynosi 0.25, co jest zdecydowanie zbyt dużą wartością. Te obliczenia wyjaśniają, dlaczego w tym konkretnym widoku pomocny jest pobliski lokalny punkt odniesienia.

Odstęp jest wskazówką diagnostyczną, a nie pełnym oszacowaniem błędu. Konwersja danych wejściowych, odejmowanie, przekształcenia i końcowa konwersja na potrzeby rysowania mogą wprowadzać błędy. Zanim zaakceptujesz zakres powiększenia, sprawdź znane przesunięcia na całej ścieżce, łącznie z właściwym rendererem.

Obserwacja Prawdopodobny problem numeryczny Następne działanie
Dodanie przesunięcia nie zmienia zapisanej współrzędnej Odstęp reprezentacji jest większy niż przyrost Przechowuj przesunięcie oddzielnie albo wybierz bliższy punkt odniesienia
Współrzędne względne działają aż do konwersji Precyzja typu docelowego jest niewystarczająca Konwertuj po odjęciu początku i zastosowaniu powiększenia; sprawdź wynikowy błąd
Wartości stają się nieskończone Operacja przekracza zakres wartości skończonych Zmień kolejność obliczeń albo ogranicz obsługiwany widok
Niezerowe przesunięcie staje się zerem Utrata przez niedomiar lub zaokrąglenie Przeskaluj wcześniej albo zmień reprezentację lokalną
Program odrzuca odległą komórkę Przekroczono zasady szczegółowego widoku Wybierz pobliski początek albo użyj osobnych, zgrubnych obliczeń dla widoku ogólnego

W widoku ogólnym część drobnych szczegółów może zgodnie z założeniami odwzorowywać się na wartość znacznie mniejszą od jednej jednostki wyświetlania. Określ jawnie, jak ma się wtedy zachowywać system. Ograniczenie przykładu do sąsiednich komórek celowo zawęża zakres dokładnych obliczeń; jego rozszerzenie wymaga nowego budżetu precyzji, a nie zwykłego usunięcia warunku ochronnego.

Co dzieje się przy bardzo małych skalach i podczas rysowania?

Apple rozróżnia leastNormalMagnitude od mniejszych dodatnich wartości podnormalnych. Wartości podnormalne mają mniejszą precyzję niż wartości normalne, dlatego najmniejsza liczba dodatnia nie jest dobrym domyślnym zakresem roboczym. Podczas mnożenia bardzo małych przesunięć przez powiększenie sprawdzaj wartości pośrednie, a nie tylko dane wejściowe. leastNormalMagnitude

Przykład odrzuca niezerowe przesunięcie, które po przeskalowaniu staje się zerem lub którego nie da się odróżnić od początku ekranu. Nie potwierdza on, że każdy niezerowy wynik mieści się w granicach tolerancji wizualnej. Dodaj kontrolę opartą na tolerancji odpowiednią dla twojego obszaru roboczego i jawnie obsługuj zgłaszane błędy zamiast zastępować wynik współrzędną początku.

W przypadku obszaru roboczego 2D zachowuj rozdzieloną pozycję dla każdej osi i oblicz obie współrzędne względne przed rysowaniem. Przy rozszerzeniu na 3D Apple udostępnia simd_double3, alias typu SIMD3<Double>. Każdy komponent pozostaje 64-bitową wartością zmiennoprzecinkową z własnym ograniczeniem precyzji. simd_double3

Zachowaj rozdzielony stan sceny jako źródło prawdy o położeniu. Konwertuj tylko końcowe, pobliskie współrzędne rysowania na typ wymagany przez API rysowania i mierz błąd konwersji względem budżetu w przestrzeni wyświetlania. Jeśli te końcowe wartości nadal są ogromne, najpierw ustal, czy dany obiekt należy do bieżącego widoku.

Stabilny obszar roboczy wymaga określenia obsługiwanego zakresu: adresów komórek typu całkowitego, ograniczonych lokalnych przesunięć, przedziału powiększenia i akceptowalnego błędu w przestrzeni wyświetlania. Zachowuj szczegóły przed składaniem dużych wartości, obliczaj względem pobliskiego początku i sprawdzaj reprezentatywne przesunięcia na granicach tego zakresu.

Źródła