Animowane tekstury modeli; Jak napisać moduł cieniujący?


9

Oglądałem trochę Rocket League i zauważyłem, że były animowane naklejki i koła.

Wizerunek.

Chciałbym zaimplementować coś podobnego do efektów na powyższym obrazku.

Jak miałbym pisać o Unity Shaderze, aby uzyskać efekt koła?

Nie wiem dużo o Shaderach, ale czy mogę edytować standardowy Unity Shader, aby uzyskać efekt animacji?


3
Shadery są tu zdecydowanie najlepszym rozwiązaniem, nawet w przypadku czegoś tak prostego jak przewijana tekstura. Unity ma wbudowaną _Timezmienną, którą można dodać do współrzędnych tekstury w module cieniującym (wierzchołek), aby uzyskać tanie efekty przewijania. Efekt sześciokąta również byłby prosty. Jeśli edytujesz pytanie, aby podświetlić tylko jeden efekt i pytasz „jak zaimplementuję to w module cieniującym Unity”, prawdopodobnie możemy ci pomóc. Pamiętaj, aby określić, czy moduł cieniujący musi reagować na oświetlenie / cieniowanie, ponieważ ma to wpływ na sposób jego pisania.
DMGregory

Dzięki za informację. Zmieniłem moje pytanie, czy to trochę lepsze? Tak naprawdę nie wiem o oświetleniu i cieniowaniu, więc na razie o tym zapomniałem, dopóki nie zrozumiem lepiej Shaderów. Chyba bym ich chciał, ale mógłbym po prostu skopiować części ze standardowego modułu cieniującego, prawda?
JacketPotatoeFan

3
Rozbuduję go w odpowiedzi nieco później wieczorem (choć możesz mnie pobić, jeśli ktoś chce teraz odpowiedzieć!) - ale zwykle piszesz moduł cieniujący Unity, który wykorzystuje oświetlenie jako „moduł cieniujący powierzchni” podczas gdy taki, który nie potrzebuje oświetlenia, często jest łatwiej napisać jako „Unlit Shader”. Wydaje mi się, że te koła nie są podświetlone (delikatne zacienienie na krawędzi wygląda jak SSAO), więc na początek zostawiłbym światło poza obrazem. Czy możesz wyjaśnić: czy chcesz dokładnie pokazany tutaj wzór geometryczny, czy możliwość przewijania dowolnych tekstur na zewnątrz i tęczowego zabarwienia?
DMGregory

Dzięki bardzo, wszelkie informacje, które możesz podać, aby zacząć, byłyby świetne. Naprawdę podziwiam ludzi, którzy potrafią pisać shadery, przejrzałem kilka artykułów dla Unity Shaders i tak, bardzo mylące. Myślę, że po prostu przewijanie tekstur (lub tekstur UV?) Byłoby wystarczające i posiadało zdolność do barwienia.
JacketPotatoeFan

Odpowiedzi:


13

Zbuduję to w kilku warstwach, abyś mógł zobaczyć, jak to się łączy.

Zacznij od utworzenia nowego modułu cieniującego w Unity, wybierając Create --> Shader --> Unlitalbo z menu Zasoby, albo z menu kontekstowego prawym przyciskiem myszy w oknie projektu.

W górnym bloku dodamy _ScrollSpeedsparametr kontrolujący szybkość ruchu tekstury w każdym kierunku:

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _ScrollSpeeds ("Scroll Speeds", vector) = (-5, -20, 0, 0)
}

To ujawnia nową 4-komponentową właściwość float w inspektorze materiałów, z przyjazną nazwą „Prędkości przewijania” (podobnie do dodawania publiclub Serializedzmiennej do MonoBehaviourskryptu)

Następnie użyjemy tej zmiennej w module cieniującym wierzchołek do przesunięcia współrzędnych tekstury ( o.uv), dodając tylko dwa wiersze do domyślnego modułu cieniującego:

sampler2D _MainTex;
float4 _MainTex_ST;
// Declare our new parameter here so it's visible to the CG shader
float4 _ScrollSpeeds;

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);

    // Shift the uvs over time.
    o.uv += _ScrollSpeeds * _Time.x;

    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

Uderz to w quad (z uroczą swobodną teksturą żyrafy autorstwa Kenneya ), a otrzymasz:

Quad z przewijającą się żyrafą.

Aby uzyskać przewijanie tekstury na zewnątrz w pierścieniu, możemy po prostu użyć siatki podzielonej jak pajęczyna, ze współrzędną UV v zwiększającą się od środka na zewnątrz. Ale to da same artefakty w kształcie piły. Zamiast tego pokażę, jak możemy obliczyć nasze promieniowanie UV na fragment.

Jest to nieco bardziej kosztowne, zarówno z powodu operacji wyzwalania i długości (które są droższe niż podstawowa matematyka) oraz ponieważ nie jest tak wydajne przewidywanie i buforowanie danych tekstur w sprzęcie podczas obliczania texcoordów na fragment, w porównaniu do samej interpolacji między wierzchołkami. Ale taki mały efekt specjalny nie jest nadmierny.

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

    // Shift the UVs so (0, 0) is in the middle of the quad.
    o.uv = v.uv - 0.5f;

    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    // Convert our texture coordinates to polar form:
    float2 polar = float2(
           atan2(i.uv.y, i.uv.x)/(2.0f * 3.141592653589f), // angle
           length(i.uv)                                    // radius
        );

    // Apply texture scale
    polar *= _MainTex_ST.xy;

    // Scroll the texture over time.
    polar += _ScrollSpeeds.xy * _Time.x;

    // Sample using the polar coordinates, instead of the original uvs.
    // Here I multiply by MainTex
    fixed4 col = tex2D(_MainTex, polar);

    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

To daje nam coś takiego (tutaj zwiększyłem parametry kafelków w materiale, aby było wyraźniej, co się dzieje - owinięcie tylko jednego powtórzenia płytki wokół koła wygląda na zniekształcone i dziwne)

Układanie żyrafy promieniście na zewnątrz od środka kwadratu

Wreszcie, aby zabarwić teksturę przewijanym gradientem, możemy po prostu dodać gradient jako drugą teksturę i pomnożyć je razem.

Najpierw dodajemy nowy parametr tekstury u góry:

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _TintTex("Tint Texture", 2D) = "white" {}
    _ScrollSpeeds ("Scroll Speeds", vector) = (-5.0, -20.0, 0, 0)
}

I zadeklaruj to w naszym bloku CGPROGRAM, aby moduł cieniujący CG mógł to zobaczyć:

sampler2D _MainTex;
float4 _MainTex_ST;

// Declare our second texture sampler and its Scale/Translate values
sampler2D _TintTex;
float4 _TintTex_ST;

float4 _ScrollSpeeds;

Następnie zaktualizuj nasz moduł cieniujący fragmenty, aby używał obu tekstur:

fixed4 frag(v2f i) : SV_Target
{
    float2 polar = float2(
           atan2(i.uv.y, i.uv.x) / (2.0f * 3.141592653589f), // angle
           length(i.uv)                                    // radius
        );

    // Copy the polar coordinates before we scale & shift them,
    // so we can scale & shift the tint texture independently.
    float2 tintUVs = polar * _TintTex_ST.xy;
    tintUVs += _ScrollSpeeds.zw * _Time.x;

    polar *= _MainTex_ST.xy;
    polar += _ScrollSpeeds.xy * _Time.x;

    fixed4 col = tex2D(_MainTex, polar);
    // Tint the colour by our second texture.
    col *= tex2D(_TintTex, tintUVs);

    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

A teraz nasza żyrafa robi się naprawdę trippy:

daleko, stary ....

Przy nieco bardziej artystycznym doborze tekstur i szybkości przewijania może to dać efekt podobny do pokazanego w pytaniu.


W wersji, którą pokazałem powyżej, możesz zauważyć dwa małe artefakty:

  • Twarze w pobliżu środka koła stają się rozciągnięte / chude / spiczaste, a następnie, gdy poruszają się na zewnątrz, stają się zgniecione / szerokie.

    To zniekształcenie występuje, ponieważ mamy stałą liczbę twarzy na obwodzie, ale obwód, który obejmują, powiększa się wraz ze wzrostem promienia, a ich wysokość pozostaje taka sama.

    Możemy to naprawić poprzez ponowne mapowanie pionowego komponentu próbki tekstury, aby podążać za krzywą logarytmiczną, więc powtórzenia tekstury są bardziej oddalone od siebie, gdy promień się zwiększa, i bliżej siebie w kierunku środka. (W rzeczywistości daje to nam nieskończony regres coraz mniejszych żyraf ...)

  • W środkowej lewej części kwadratu znajduje się rząd jednego lub dwóch rozmytych pikseli.

    Dzieje się tak, ponieważ GPU patrzy na dwie sąsiednie współrzędne próbki tekstury, aby dowiedzieć się, jakiego filtrowania użyć. Gdy próbki są blisko siebie, tekst jest wyświetlany jako duży / zamknięty i pokazuje najbardziej szczegółowy poziom mip. Kiedy próbki są daleko od siebie, domyślamy się, że musimy pokazywać teksturę z niewielkim powiększeniem lub z dużej odległości, i pobiera próbki z mniejszej / rozmytej mipmapy, aby upewnić się, że nie otrzymamy błyszczących artefaktów aliasingowych.

    Problem jest tutaj, jesteśmy w punkcie zawinięcia we współrzędnych biegunowych, od -180 do 180 stopni. Próbujemy więc z bardzo podobnych punktów w naszej powtarzalnej przestrzeni tekstur, nawet jeśli ich współrzędne numeryczne sprawiają, że wyglądają, jakby były daleko od siebie. Możemy więc dostarczyć własne wektory gradientu próbkowania, aby to skorygować.

Oto wersja z następującymi poprawkami:

fixed4 frag(v2f i) : SV_Target
{
    float2 polar = float2(
           atan2(i.uv.y, i.uv.x) / (2.0f * 3.141592653589f), // angle
           log(dot(i.uv, i.uv)) * 0.5f                       // log-radius
        );

    // Check how much our texture sampling point changes between
    // neighbouring pixels to the sides (ddx) and above/below (ddy)
    float4 gradient = float4(ddx(polar), ddy(polar));

    // If our angle wraps around between adjacent samples,
    // discard one full rotation from its value and keep the fraction.
    gradient.xz = frac(gradient.xz + 1.5f) - 0.5f;

    // Copy the polar coordinates before we scale & shift them,
    // so we can scale & shift the tint texture independently.
    float2 tintUVs = polar * _TintTex_ST.xy;
    tintUVs += _ScrollSpeeds.zw * _Time.x;

    polar *= _MainTex_ST.xy;
    polar += _ScrollSpeeds.xy * _Time.x;

    // Sample with our custom gradients.
    fixed4 col = tex2Dgrad(_MainTex, polar, 
                           _MainTex_ST.xy * gradient.xy,
                           _MainTex_ST.xy * gradient.zw
                         );

    // Since our tint texture has its own scale,
    // its gradients also need to be scaled to match.
    col *= tex2Dgrad(_TintTex, tintUVs,
                          _TintTex_ST.xy * gradient.xy,
                          _TintTex_ST.xy * gradient.zw
                         );

    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

2
Kilka drobnych ulepszeń, które można wprowadzić: 1) użycie krzywej logarytmicznej dla kierunku v pomaga tekstury zachować jej kształt podczas przewijania na zewnątrz (zamiast zwężania do punktu pośrodku, otrzymujesz nieskończony łańcuch mniejszych kopii, aż mip rozmazuje go) 2) obliczenie własnych gradientów tekstury i użycie tex2Dgradnaprawia artefakt filtrowania, w którym kąt zawija się od -pi do + pi (to jest cienka linia rozmytych pikseli po lewej stronie). Oto
poprawiony

Niesamowite, dziękuję również za awarię. Wydaje mi się, że Shadery są czymś, czego nigdy nie będę w stanie napisać (szczególnie rzeczy, które zrobiłeś dla „polarnego”, matematyka nic dla mnie nie znaczy, ponieważ mam tylko bardzo podstawowe umiejętności matematyczne).
JacketPotatoeFan

1
Takie rzeczy zwykle ludzie po prostu patrzą w górę. ;) Po prostu robię to na tyle, że zsuwa się z klawiatury. Spróbuj jednak bawić się różnymi funkcjami matematycznymi i przyjrzyj się wynikom - posiadanie takiego narzędzia, które pomoże mi w wizualizacji tego, co matematyka robi geometrycznie, jest przede wszystkim sposobem na zdobycie praktyki. (Nie dotyczy to konkretnie shaderów, ale starego programu do wizualizacji WinAmp o nazwie WhiteCap ...) Nawet jeśli matematyka modułu cieniowania okropnie się psuje, często jest to dziwnie fajne. : D
DMGregory
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.