Wczoraj wrzuciłam film pokazujący jaki efekt chciałam osiągnąć. Dzisiaj rozbicie tego efektu na części pierwsze.
- Wybór i załadowanie obrazka z dysku
- Dopasowanie zawartości obrazka do wymaganej wielkości
- Przycięcie obrazka, zachowanie przyciętego wyniku w odpowiedniej teksturze.
- Nałożenie nowej tekstury na Sprite, przy jednoczesnym zachowaniu wymiarów rzeczonego Sprite’a
- Upewnienie się, że dostępna tekstura będzie cały czas owalna, tak żeby ładnie mieściła sie w ramce
Wybór i załadowanie obrazka z dysku
Nad tym punktem nie zamierzam sie rozwodzić. W wypadku systemu Windows można po prostu użyć OpenFileDialog
, w wypadku pozostałych platform na pewno znajdziecie jakieś wtyczki. Ja użyłam tej: Unified Android API, niekoniecznie dlatego że jest dobra, ale była pierwsza z brzegu i darmowa, a ja jestem dośc leniwa, nie chciało mi się pisać samodzielnie. Mozna też spróbowac skleić to samemu, nie powinno być trudne. Potencjalne linki TU i TU.
Dopasowanie zawartości obrazka do wymaganej wielkości
Krok 1 – ułożenie elementu w scenie
Obrazek który będziemy przycinać, jest ładowany do elementu ChosenImage. Element ImageCropSpot powinien mieć odpowiedni stosunek szerokości do wysokości, bo do niego będziemy przycinać.
Krok 2 – czy oba palce znajdują sie na obrazku?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private bool BothTouchesOnImage () { Vector2 checkVector; RectTransformUtility.ScreenPointToLocalPointInRectangle (image.transform.parent.GetComponent (), Input.GetTouch (0).position, mainCamera, out checkVector); //normalize the vector, so that we get values from 0 to 1. If we were not to add pivot, we would get values from -0.5 to 0.5 for pivot = (0.5, 0.5), different for other pivots checkVector = new Vector2 (checkVector.x / image.rectTransform.rect.width, checkVector.y / image.rectTransform.rect.height) + image.rectTransform.pivot; if (checkVector.x < 0 || checkVector.x > 1 || checkVector.y < 0 || checkVector.y > 1) { return false; } RectTransformUtility.ScreenPointToLocalPointInRectangle (image.rectTransform, Input.GetTouch (1).position, mainCamera, out checkVector); checkVector = new Vector2 (checkVector.x / image.rectTransform.rect.width, checkVector.y / image.rectTransform.rect.height) + image.rectTransform.pivot; if (checkVector.x < 0 || checkVector.x > 1 || checkVector.y < 0 || checkVector.y > 1) { return false; } return true; } |
Krok 3 – zwiększanie/zmniejszanie rozmiaru obrazka
Podzieliłam to na dwie klasy – jedna z nich upewnia się że ScrollRect w którym znajduje się przeskalowywany obrazek jest wyłączony podczas skalowania. Jesli nie byłby on wyłączony, obrazek przeskakiwał by podczas skalowania, bo ScrollRect próbował by go przesunąć. Druga klasa po prostu zwieksza/zmniejsza obrazek, mając na uwadze aby jego wielkość w pikselach nie była mniejsza od zadanej (w szczególności nie mniejsza niż obrazek do którego będziemy przycinać).
Kolejna ważna rzecz: pivot. Na początku pivot obrazka jest ustawiony na (0.5, 0.5), czyli na sam jego środek. W związku z tym, podczas zwiększania/zmniejszania, obrazek będzie sie skalował „od środka”, co może wyglądać trochę nienaturalnie dla użytkownika. Zazwyczaj oczekuje się, ze podczas skalowania elementem niezmiennym jest element znajdujący się równo pośrodku palców które „rozciągają” obrazek.
Klasy sa bardzo długie i bardzo nudne, więc zamiast wklejać je w całości, poniżej linki do githuba.
https://github.com/ania1234/FencingEars/blob/master/Assets/Scripts/ZoomHelper.cs
https://github.com/ania1234/FencingEars/blob/master/Assets/Scripts/ZoomableImage.cs
Przycięcie obrazka, zachowanie przyciętego wyniku w odpowiedniej teksturze.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void PrepareNewTexture () { Rect cutRect = imageCutRectTransform.rect; Rect imageRect = mainTextureImage.rectTransform.rect; Vector3[] cutRectCorners = new Vector3[4]; imageCutRectTransform.GetWorldCorners (cutRectCorners); Rect newCutRect = new Rect (cutRectCorners [0], cutRectCorners [2] - cutRectCorners [0]); Vector3[] imageRectCorners = new Vector3[4]; mainTextureImage.rectTransform.GetWorldCorners (imageRectCorners); Rect newImageRect = new Rect (imageRectCorners [0], imageRectCorners [2] - imageRectCorners [0]); //Debug.Log ("New Image rect starts at x = " + (newImageRect.xMin * mainTextureImage.rectTransform.localScale.x).ToString () + " y = " + (newImageRect.yMin * mainTextureImage.rectTransform.localScale.y).ToString () + "and has width = " + (newImageRect.width).ToString () + " height " + (newImageRect.height).ToString ()); //Debug.Log ("Image rect starts at x = " + (imageRect.xMin * mainTextureImage.rectTransform.localScale.x).ToString () + " y = " + (imageRect.yMin * mainTextureImage.rectTransform.localScale.y).ToString () + "and has width = " + (imageRect.width * mainTextureImage.rectTransform.localScale.x).ToString () + " height " + (imageRect.height * mainTextureImage.rectTransform.localScale.y).ToString ()); Vector2 percentFromLowerLeftCorner = new Vector2 (Mathf.Abs (newImageRect.xMin - newCutRect.xMin) / newImageRect.width, Mathf.Abs (newImageRect.yMin - newCutRect.yMin) / newImageRect.height); int startx = Mathf.FloorToInt (percentFromLowerLeftCorner.x * mainTextureImage.mainTexture.width); int starty = Mathf.FloorToInt (percentFromLowerLeftCorner.y * mainTextureImage.mainTexture.height); float percentageWidth = newCutRect.width / newImageRect.width; float percentageHeight = newCutRect.height / newImageRect.height; Texture2D finalTexture = new Texture2D (Mathf.FloorToInt (mainTextureImage.mainTexture.width * percentageWidth), Mathf.FloorToInt (mainTextureImage.mainTexture.height * percentageHeight)); finalTexture.SetPixels (((Texture2D)mainTextureImage.mainTexture).GetPixels (startx, starty, Mathf.FloorToInt (mainTextureImage.mainTexture.width * percentageWidth), Mathf.FloorToInt (mainTextureImage.mainTexture.height * percentageHeight))); finalTexture.Apply (); TextureManager.portraitTexture = finalTexture; } |
Nałożenie nowej tekstury na Sprite, przy jednoczesnym zachowaniu wymiarów rzeczonego Sprite’a
Po wykonaniu powyższych kroków, w zmiennej statycznej TextureManager.portraitTexture znajduje sie przycięty obrazek. Jego stosunek jego wymiarów jest taki jak w oryginalnym obrazku, ale jego wymiary w pikselach moga się różnić. Ponieważ chcemy zachować oryginalne wymiary wyświetlonego sprite’a, należy zmienić rozdzielczość dla nowego Sprite’a, czyli pomanipulować przy pixelsPerUnit.
1 2 3 4 5 |
if (TextureManager.portraitTexture != null) { //We have to calculate new pixels per unit, so that the new sprite will be the same size as the previous one. float newPixelsPerUnit = spriteRenderer.sprite.pixelsPerUnit * TextureManager.portraitTexture.width / spriteRenderer.sprite.rect.width; spriteRenderer.sprite = Sprite.Create (TextureManager.portraitTexture, new Rect (new Vector2 (0, 0), new Vector2 (TextureManager.portraitTexture.width, TextureManager.portraitTexture.height)), new Vector2 (0.5f, 0.5f), newPixelsPerUnit); } |
Upewnienie się, że dostępna tekstura będzie cały czas owalna, tak żeby ładnie mieściła sie w ramce
Postanowiłam trochę zmodyfikować SpriteShader wbudowany w unity. Dodatkowo przyjmuje teraz teksturę _MaskTex
. Nieprzezroczyste miejsca na teksturze maski są renderowane jako przezroczyste. Zastosowane rozwiązanie działa wtedy, keidy _MainTex
i _MaskTex
mają taki sam stosunek długości boków. Kod poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
Shader "Custom/MaskSpriteShader" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _MaskTex ("Mask Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off Lighting Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO }; fixed4 _Color; v2f vert(appdata_t IN) { v2f OUT; UNITY_SETUP_INSTANCE_ID(IN); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; OUT.color = IN.color * _Color; #ifdef PIXELSNAP_ON OUT.vertex = UnityPixelSnap (OUT.vertex); #endif return OUT; } sampler2D _MainTex; sampler2D _MaskTex; sampler2D _AlphaTex; fixed4 SampleSpriteTexture (float2 uv) { fixed4 color = tex2D (_MainTex, uv); #if ETC1_EXTERNAL_ALPHA // get the color from an external texture (usecase: Alpha support for ETC1 on android) color.a = tex2D (_AlphaTex, uv).r; #endif //ETC1_EXTERNAL_ALPHA //get the alpha from mask texture color.a *= 1-tex2D(_MaskTex, uv).a; return color; } fixed4 frag(v2f IN) : SV_Target { fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb *= c.a; return c; } ENDCG } } } |