2011-08-16

Supersampling

Supersamling polega na tym by na każdy piksel obrazu wygenerować więcej subpikseli niż nam potrzeba, a następnie uśrednić ich wartość. Uśrednianie to proces resamplingu i istnieje wiele metod, a w zasadzie filtrów resamplujących. W najprostszej postaci bierzemy średnią ze wszystkich subpikseli.

Supersampling + resampling pozwala nam na polepszenie jakości sceny. Usuwamy z niej schodki na figurach, schodki na teksturach, interferencje na teksturach, które manifestują się jeśli tekstura jest jakimś regularnym wzorem.

Przypuśćmy, że mamy sygnał dźwiękowy próbkowany z częstotliwością 44KHz (przez analogię możemy go traktować jako linię obrazu). Chcemy go przekształcić w sygnał próbkowany z częstotliwością 14KHz (przez analogię nasz element tekstury zajmuje niewielki obszar ekranu, trzeba go mniej więcej 3x pomniejszyć). Uwaga próbkowanie z częstotliwością 44KHz oznacza, że sygnał niesie informację o częstotliwości 22KHz i mniejszych. Jeśli wybierzemy co mniej więcej 3 próbkę dźwięk będzie zniekształcony. Do sygnału wyjściowego wkradną się składowe widma o częstotliwościach powyżej 7KHz. Dokładniej zostaną one odbite tak, że 8KHz pojawi się na 6KHz, 16KHz pojawi się na 5KHz. I już nic z tym nie zrobimy, nie jesteśmy w stanie odróżnić prawdziwego sygnału 5KHz od tego odbitego. Aby tego uniknąć przed downsamplingiem sygnał filtruje się filtrem dolnoprzepustowym o częstotliwości 7KHz. Tak robi się w przypadku dźwięku i filtrów resamplujących typu Lanczos i Bicubic, które są pewnym przybliżeniem filtru dolnoprzepustowego. Generalnie w przypadku dźwięku wszelkie przybliżenia są niepożądane, gdyż ucho ludzkie w przeciwieństwie do oka analizuje widmo sygnału. W przypadku obrazu możemy wziąć 3 próbki i je uśrednić (to ciągle jest swego rodzaju dosyć niedoskonały filtr dolnoprzepustowy), w przypadku dźwięku nie.

Czyli myśląc o wygładzaniu, filtrowaniu, samplowaniu, resamplowaniu powinniśmy raczej myśleć o częstotliwościach. To, że zwykłe uśrednianie też działa (ale nie daje dobrego rezultatu) wynika z faktu jak działa oko.

Mamy sinusoidę powiedzmy o f=1KHz, próbkowanie fs=44KHz. Nasza sinusoida ewidentnie ma schodki. Ale do reprodukcji tych schodków potrzeba częstotliwości wyższych niż 44KHz, a tych nie w naszym sygnale. Jeśli taki schodek z DAC puścimy na głośniku to będziemy mieć zniekształcenia, ale jeśli użyjemy filtru dolnoprzepustowego 22KHZ to na wyjściu zostanie w idealnym świecie pojedyncza sinusoida (pik w domenie częstotliwości).

Innymi słowy filtr dolnoprzepustowy wygładził nam schodki usuwając z sygnału wyższe częstotliwości. Dokładnie to samo robimy w resamplingu.

By móc bez zniekształceń przedstawić sygnał o wyższej częstotliwości w postaci sygnału o niższej częstotliwości musimy mieć więcej próbek niż potrzebujemy.

Idealnie chcąc uniknąć jakichkolwiek błędów potrzebujemy po próbkę na każdy piksel tekstury inaczej zawsze już podczas supersamplingu wprowadzamy do sygnału zakłócenia (aliasing). Niestety z uwagi na złożoność obliczeniową jest to dosyć kłopotliwe. Dlatego np. przyjmując że samplujemy 9x9 subpikseli na piksel nasz obraz wzorcowy posiada już w sobie zakłócenia. Proces resamplingu nie usuwa ich. Pozwala nam się pozbyć tych związanych z częstotliwościami które są w 9x9, a nie powinno ich być w 1x1. W zależności od odległości tekstury od ekranu usuwamy tym tylko pewną część zakłóceń, ale można powiedzieć tą najważniejszą znacząco poprawiającą obraz.

Supersampling działa kompleksowo na całą scenę. Na tekstury (wygładzanie + interferencje), na brzegi obiektów (wygładzenie), na wiele małych obiektów równo rozłożonych.

Z uwagi na to, że nie wszystkie piksele w obrazie końcowym wymagają takiego samego supersamplingu stosuje się samplowanie adaptacyjne, w którym uwzględnia się różnice pomiędzy sąsiednimi pikselami i jeśli jest ona duża to obszar dzieli się na mniejsze subpiksele. To problem na zupełnie inny wpis i czas (implementacji).

By walczyć z zakłóceniami wprowadzanymi podczas tworzenia obrazu subpikseli stosuje się samplowanie losowe. Staramy się nasze promienie wysyłać nieregularnie rozłożone.

Jeśli promienie są generowane regularnie w każdy subpiksel, to mamy tzw. samplowanie typu Grid, jeśli wysyłamy je losowo, ale ciągle w granicach subpiksela to mamy samplowanie typu Jitter.

Grid:


  
  
  
    
      
        image/svg+xml
        
        
      
    
  
  
    
    
    
    
    
    
    
    
    
    
    
    
    
  

Jitter:


  
  
  
    
      
        image/svg+xml
        
        
      
    
  
  
    
    
    
    
    
    
    
    
    
    
    
    
    
  

Zupełnie inną klasą samplerów są te które nie wysyłają po jednym promieniu na subpiksel. Mówimy wtedy o samplowanie niejednorodnym typu Random (zupełnie losowe), Poisson Disk (losowe, ale równomiernie rozmieszczone, podobnie jak czopki w oku).

Uwaga supersampling+resampling, a filtrowanie tekstur (też wygładzanie) to zupełnie co innego. W filtrowaniu tekstur wyliczamy wartości koloru w konkretnej współrzędnej zmiennoprzecinkowej wykorzystując wartości w punkach całkowitych (lepiej o punktach myśleć jak o kwadracikach o takim samym kolorze). Informacje którą generujemy tam nie ma. Zgadujemy co znajduje się w punktach pośrednich. Inaczej niż do supersamplingu gdzie wartości subpikseli są wyliczane. Filtrowanie wygładzi nam teksturę ale nie usunie interferencji i schodków na brzegach obiektów.

Poniżej kod samplerów typu Jitter i Grid. Innych na razie nie implementowałem. W przyszłości może pojawi się samplowanie adaptywne. Niejednorodne to śpiew przyszłości.

Kod:
public enum SampleMethod
{
    Grid,
    Jitter
}

internal class PixelInfo
{
    /// <summary>
    /// Within RenderOptions.Width and RenderOptions.Height.
    /// </summary>
    public readonly Vector2 RayPos;

    /// <summary>
    /// Within RenderOptions.Width and RenderOptions.Height times sampling size.
    /// </summary>
    public readonly Vector2 PixelPos;

    public PixelInfo(Vector2 a_ray_pos, Vector2 a_pixel_pos)
    {
        Debug.Assert(a_pixel_pos.X.Fraction().IsZero());
        Debug.Assert(a_pixel_pos.Y.Fraction().IsZero());

        RayPos = a_ray_pos;
        PixelPos = a_pixel_pos;
    }
}

internal abstract class Sampler
{
    private RenderOptions m_render_options;

    public static Sampler Create(RenderOptions a_render_options)
    {
        if (a_render_options.SampleMethod == SampleMethod.Grid)
            return new GridSampler(a_render_options);
        if (a_render_options.SampleMethod == SampleMethod.Jitter)
            return new JitterSampler(a_render_options);
        else
            throw new InvalidOperationException();
    }

    public Sampler(RenderOptions a_render_options)
    {
        m_render_options = a_render_options;
    }

    public IEnumerable<PixelInfo> GetSamples()
    {
        return GetSamples(new Rectangle(0, 0, m_render_options.Width,
            m_render_options.Height));
    }
    public IEnumerable<PixelInfo> GetSamples(Rectangle a_rect)
    {
        for (int y = a_rect.Top; y < a_rect.Bottom; y++)
        {
            for (int yy = 0; yy < m_render_options.SampleSize; yy++)
            {
                for (int x = a_rect.Left; x < a_rect.Right; x++)
                {
                    for (int xx = 0; xx < m_render_options.SampleSize; xx++)
                    {
                        yield return CreatePixelInfo(x, y, xx, yy,
                            m_render_options.SampleSize);
                    }
                }
            }
        }
    }

    protected abstract PixelInfo CreatePixelInfo(int a_x, int a_y, 
        int a_xx, int a_yy, int a_size);
}

internal class JitterSampler : Sampler
{
    private Random m_random;

    public JitterSampler(RenderOptions a_render_options)
        : base(a_render_options)
    {
        m_random = new Random(a_render_options.RandomSeed);
    }

    protected override PixelInfo CreatePixelInfo(int a_x, int a_y, 
        int a_xx, int a_yy, int a_size)
    {
        return new PixelInfo(
            new Vector2(
                a_x + (a_xx + m_random.NextDouble()) / a_size,
                a_y + (a_yy + m_random.NextDouble()) / a_size),
            new Vector2(a_x * a_size + a_xx, a_y * a_size + a_yy)
        );
    }
}

internal class GridSampler : Sampler
{
    public GridSampler(RenderOptions a_render_options)
        : base(a_render_options)
    {
    }

    protected override PixelInfo CreatePixelInfo(int a_x, int a_y, 
        int a_xx, int a_yy, int a_size)
    {
        return new PixelInfo(
            new Vector2(
                a_x + (a_xx + 0.5) / a_size,
                a_y + (a_yy + 0.5) / a_size),
            new Vector2(a_x * a_size + a_xx, a_y * a_size + a_yy)
        );
    }
}
Przykłady samplowania z różną dokładnością. Resamplowanie zawsze typu Box. Zauważmy, że Jitter nadaję się tylko dla większej ilości próbek, gdyż inaczej brzegi są poszarpane. Widzimy, że czy większa ilość próbek tym zakłócenia pojawiają się coraz bliżej horyzontu, w momencie w którym częstotliwość próbkowania zrównuje się z częstotliwością biało-czerwonej siatki.


Grid 1x1:



Jitter 1x1:



Grid 3x3:



Jitter 3x3:



Grid 6x6:



Jitter 6x6:



Grid 9x9:



Jitter 9x9:



Brak komentarzy:

Prześlij komentarz