반응형

셰이더는 디바이스를 타는 경우가 많아 코드 작성시 주의 해야할 사항이 많습니다.

얼마전에 공유드렸던 vert_img가 노트4에서만 죽는 거 처럼 안드로이드 기기는 워낙 다양한 GPU를 사용하다보니

로그에 남지 않는 크래쉬 발생이 많고, 또한 어디서는 정상으로 나오던 것들이 어디서는 잘못 렌더 되는 경우가 많습니다.


최근에는 디스토션 셰이더가 아이폰 6에서만 화면이 어둡게 나오는 현상이 발견되어 해당 셰이더를 못쓰는 상황도 발생했습니다..ㅠ


이번에 공유 드릴 내용은 셰이더코드를 어떻게 작성하느냐에 따라 프레임렉을 유발할수 있는 부분에 대해서 나누고자 합니다.


물 셰이더를 제작하면서 버텍스 연산하는 부분을 함수로 따로 뺀뒤 연산하도록 작업이 됐었는데,

갤럭시 시리즈에서만 버텍스 애니메이션시 프레임 렉을 일으키는 문제가 발생했습니다. LG시리즈나 아이폰에서는 이상이 없는데..



아래는 버텍스 연산 내용을 처리한 셰이더 코드 입니다.



fixed getWave(fixed3 s)

{

fixed time = _Time.y * _WaveSpeed;

fixed z = sin(time + s.y * _WaveCycle) * _WavePow;

return z;

}

v2f vert(appdata_base v)

{

v2f o;

v.vertex.z += getWave(v.vertex);


  return o;

 }



-- 프레임 렉을 줄이는 방법--


1. 타임함수의 y로 계산하지 않고 x로 계산하고 *20을 해주면 소수값 연산이 줄어들어 프레임렉을 줄일 수 있습니다.


2. Time을 사용하지 않고 sin을 사용하면 프레임렉을 줄일수 있다. 단, 연산부하가 늘어납니다.


3. 아래처럼 함수로 따로 빼지 않고 버텍스 연산하는데 포함 시키면 프레임렉을 어느정도 완화시킬수 있습니다.


v2f vert(appdata_base v)

{

v2f o;


fixed time = _Time.y * _WaveSpeed;

fixed zzz = sin(time + v.vertex.y) * _WavePow;


v.vertex.z += zzz;


   return o;

}




반응형
반응형

바닥 아웃라인 셰이더를 만든후 그 위에 올라갈 오브젝트의 소팅에 문제가 있다는 것을 발견했습니다.

 

 

이 때 사용된 렌더큐를 따져보면

 

바닥 렌더 큐(Pink) - Tags {"Queue"="Transparent+100" "IgnoreProjector"="True" "RenderType"="Transparent"}

 

오브젝트 렌더 큐(Blue) - Tags {"RenderType"="Opaque"}

 

큐 순서를 따져보면 Transparnet 가 Opaque보다 소팅이 위에 찍히기에 당연한 결과라 할 수 있습니다.

 

그래서 오브젝트 렌더 큐를 아래와 같이 변경을 해보았습니다.

 

 

Tags {"Queue"="Transparent+200" "IgnoreProjector"="True" "RenderType"="Transparent"}

 

그리고 유니티로 돌아왔지만, 여전히 소팅 문제는 해결되지 않았습니다.

원인을 알수 없어 이짓 저짓 다해보고 서피스 방식이었던 셰이더를 프라그로 바꿔보고 ZTest도 바꿔보고 다 해보았지만

여전히 바닥아래로 오브젝트가 찍히는 것입니다.

 

결국 뒤지다 뒤지다 알게 된 사실이 렌더 큐에 대한 것은 코드를 수정해도 실시간으로 바로 바뀌는게 아님을 알게 되었습니다.

 

셰이더가 생성되는 순서가 먼저이고 그 다음에 큐에 대한 것을 연산하는 것입니다.

그래서 다른 셰이더로 한번 바꾼후 다시 원래 셰이더로 돌아오니 큐가 제대로 찍히는 것입니다.

 

 

 

이게 버그인지 아니면 의도된 것인지 모를 일입니다.. 요상한 유니티..

 

 

 

반응형
반응형

유니티의 아웃라인 셰이더는 구글에 돌아다니는 레퍼런스가 많아 손쉽게 구현이 가능합니다.

하지만, 자세하게 여러가지를 따지다 보면 불편한 사항이나 제약이 많이 뒤따른다는 것을 쉽게 발견할 수 있습니다.

 

 

일반적인 공식으로 알려져있는 아래 수식대로 셰이더를 만들어 아래 오브젝트에 연결해 보면..

 

        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        fixed3 norm   = mul ((fixed3x3)UNITY_MATRIX_MVP, v.normal);
        fixed2 offset = TransformViewToProjection(norm.xyz);
     
        o.pos.xy += offset * o.pos.z * _Outline;

        o.pos.z += 0.01 * _Outline;


        o.color = _OutlineColor;
        return o;

 

 

 

3D형태의 메쉬를 가진 오브젝트에는 잘 적용되지만 플랜 상태의 오브젝는 제대로 출려되지 않는 문제가 발생합니다. 아래 그림 참조

 

 

 

이 문제를 해결하기 위해서는 버텍스의 노멀 값 뿐 아니라 사이즈자체도 커지도록 한번 더 계산해 주는 수식을 넣어줘야 합니다.

 

fixed3 norm   = mul ((fixed3x3)UNITY_MATRIX_MVP, v.normal); //요 수식에서

 

fixed3 norm   = mul ((fixed3x3)UNITY_MATRIX_MVP, v.vertex + v.normal); //버텍스 값이 더해지도록 수식을 추가해 준다.

 

 

이렇게해도 제대로 출력되지 않을땐 아웃라인 패스 부분에 Cull back 을 추가해 줍니다.

 

 

 

그러면 아래 그림처럼 플랜에서도 정상적인 아웃라인을 만들어 지게 됩니다.

 

 

 

 

하지만 이 셰이더를 사용할 때 주의해야할 점이 있는데 바로 배칭이 안된다는 것입니다.

 

스태틱 배칭을 키고나면 버텍스 연산이 다르게 되어 아래그림처럼 엉뚱한 형태로 출력되게 됩니다.

 

 

 

위 셰이더를 사용하고자 할 때는 메터리얼을 따로 만들어 관리하는 것이 최적화에 도움이 될겁니다.

 

 

 

아래는 위 오브젝트의 아웃라인을 구현한 셰이더 코드입니다.

 

{
    Properties {
        _MainColor ("Main Color"Color) = (.5,.5,.5,1)
        _OutlineColor ("Outline Color"Color) = (0,0,0,1)

        _Outline ("Outline width"Range (010)) = 1
        _MainTex ("Base (RGB)"2D) = "white" { }
        _Rim ("Rim"Float) = 0.0
        _scale("Z Position(Only Plane)"FLoat) = 1.0

        //Sorting
        _ZTest ("ZTest Less = 4 / Always = 6 "FLoat) = 1
    }
 
    CGINCLUDE
    #include "UnityCG.cginc"
    #pragma target 3.0

    struct v2f {
        fixed4 pos : SV_POSITION;
        fixed4 color : COLOR;

    };
     
    uniform fixed _Outline;
    uniform fixed4 _OutlineColor;
    uniform fixed _Rim;
    uniform fixed _scale;
     
    v2f vert(appdata_base v) {
        
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        v.vertex.xyz += v.normal.xyz * _scale;
        fixed3 norm   = mul ((fixed3x3)UNITY_MATRIX_IT_MV, v.normal + v.vertex * _Outline);
        fixed2 offset = TransformViewToProjection(norm.xyz);
     
        o.pos.xy += (offset * o.pos.z * _Outline) * 0.001;

        o.pos.z += 0.01 * _Outline;


        o.color = _OutlineColor;
        return o;
    }
    ENDCG
 
    SubShader {
        Tags {"Queue"="Geometry+200" "IgnoreProjector"="True" "RenderType"="Opaque"}
         Blend One Zero
//         ZTest NotEqual
//         ZWrite on
        Pass
        {
            ColorMask 0
        }
//
        Pass
        {
            Name "OUTLINE"
            Tags { "LightMode" = "Always" }
            Cull off
            Blend SrcColor OneMinusSrcColor
            ZTest Always
//            ZWrite off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            fixed4 frag( v2f i ) : COLOR
            {
                fixed4 c = i.color;

                return i.color;
            }
            ENDCG
        }         
             
            CGPROGRAM
            #pragma surface surf Lambert

            struct Input {
                fixed2 uv_MainTex;
                fixed3 worldPos;
            };


            sampler2D _MainTex;
            //sampler2D _BumpMap;
            uniform fixed3 _MainColor;
            void surf(Input IN, inout SurfaceOutput o) {
//                IN.worldPOS.z;
                o.Emission = tex2D(_MainTex, IN.uv_MainTex).rgb * _MainColor * _Rim;
            //    o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
            }
            ENDCG
 
    }

}

반응형
반응형

간단한 서피스 커스텀 셰이더를 만들었는데 알파 값을 0으로 아무리 해도 알파가 안빠지는 현상이 계속됐다.

여러가지 테스트를 해보니 SurfaceOutputCustom 에 Normal 과 Emission 값을 넣어주지 않으면 알파 값이 계속 빠지지 않는 것이다..

#pragme에 keepalpha 문을 넣어주는 것은 당연하고..

 

서피스 셰이더 제작후 알파값이 빠지지 않으면 아래의 세 구문을 꼭 넣어주자.

 

Blend One OneMinusDstAlpha

 

#pragma surface surf keepalpha

 

struct SurfaceOutputCustom
{
      fixed3 Albedo;
      fixed3 Normal;
      fixed3 Emission;
      fixed Alpha;
};

 

반응형
반응형

프로젝트를 진행하면서 파티클 빌트인 셰이더들이 문제를 일으켜 일괄 변환해 줘야 하는 상황이 발생했습니다.

일일이 바꾸는 번거러움을 덜고자 제작한 스크립트 입니다.

 

 

1. 모바일 엔진 셰이더(바뀌어야할 셰이더 선택)

2. 바뀔 셰이더 선택

3. 대상 메터리얼들 선택(폴더 / 파일)

4. 대상 메터리얼중 1번과 동일 셰이더를 찾아 2번으로 바꿔줌~ 일괄 교체~

 

 

 

 

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class MaterialControl : EditorWindow
{  
    public Shader shader1 = null;
    public Shader shader2 = null;

    public Color defaultColor;
    public Color tintColor = new Color(0.5F, 0.5F, 0.5F, 0.5F);

    Shader defaultShader = null;
    Shader changeShader = null;

    static float UIWidth = 500;
    static string OutputPath = string.Empty;
    
    bool IsError = false;
    List<string> tempPathList = new List<string>();
    List<string> InputPaths = new List<string>();
    
    string SearchPattern = "*.mat;*.*";


//    ShaderImporter texFormatSelected = ShaderImporter.GetAtPath;
//    TextureImporterFormat texFormatToChange = TextureImporterFormat.ETC2_RGBA8;

    [MenuItem("RK Tools/Art/MaterialControl")]
    static void InitWindow()
    {
        MaterialControl window = EditorWindow.GetWindow<MaterialControl>();
        window.position = new Rect(100f, 100f, UIWidth, 400f); //  초기 사이즈
        
        window.title = "Save Bundle";
        window.Show();        
    }
    
    void OnGUI()
    {
        FunctionManual();
        ShaderSetting();
        SelectOutPath();
        ClearSelectFolder();
        OutputSelectedFile();
        ConvertAtlasImage();
    }
    
    void FunctionManual()
    {
        GUILayout.Space(16);
        GUI.contentColor = Color.cyan;
        EditorGUILayout.LabelField("폴더를 선택해서 해당 폴더 내의 Shader를 변경합니다.");
        GUI.contentColor = Color.white;
        GUILayout.Space(8);
    }

    void ShaderSetting()
    {
        defaultShader = (Shader)EditorGUILayout.ObjectField("Original Shader", shader1, typeof(Shader));
        changeShader = (Shader)EditorGUILayout.ObjectField("Change Shader", shader2, typeof(Shader));

        defaultColor = EditorGUILayout.ColorField("Tint Color", tintColor);

        if (defaultShader != null)
        {
            shader1 = defaultShader;
            shader2 = changeShader;
            tintColor = defaultColor;
        }


    }

    string prevPath = Application.dataPath;


    void SelectOutPath()
    {
        if (GUILayout.Button("폴더 or 파일 선택"))
        {            
            GetAssetPath_Project();            
            Repaint();
        }
    }
    
    void GetAssetPath_Project()
    {        
        SelectionProcess();
        
        foreach (string path in tempPathList)
        {
            if (InputPaths.Exists(assetPath => (assetPath == path)))
                continue;
            InputPaths.Add(path);
        }        
        IsError = false;
    }
    
    bool SelectionProcess()
    {
        tempPathList.Clear();
        string AssetPath = string.Empty;
        foreach (UnityEngine.Object obj in Selection.objects)
        {
            AssetPath = AssetDatabase.GetAssetPath(obj);
            if (Directory.Exists(AssetPath))
            {
                tempPathList.AddRange(
                    AssetDatabase.FindAssets("t:Material", new string[] { AssetPath }));
                
                for (int i = 0; i < tempPathList.Count; ++i)
                {
                    tempPathList[i] = AssetDatabase.GUIDToAssetPath(tempPathList[i]);                    
                }
            }
            else if (File.Exists(AssetPath))
            {
                char[] separate = new char[] { ';', '*', '.' };
                foreach (string pattern in SearchPattern.Split(separate))
                {
                    if (AssetPath.ToLower().Contains(pattern))
                    {
                        tempPathList.Add(AssetPath.Replace(Application.dataPath, "Assets"));
                        break;
                    }
                }
            }
        }
        return true;
    }
    
    void ClearSelectFolder()
    {
        if (GUILayout.Button("선택 폴더 or 파일 해제"))
        {
            tempPathList.Clear();
            InputPaths.Clear();
        }
    }
    
    Vector2 scrollPos = new Vector2();
    void OutputSelectedFile()
    {
        EditorGUILayout.LabelField("선택 폴더");
        
        EditorGUILayout.BeginVertical("box", GUILayout.Width(UIWidth));
        scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Width(UIWidth));
        
        foreach (string path in InputPaths)
        {
            EditorGUILayout.LabelField(path);
        }
        EditorGUILayout.EndScrollView();
        EditorGUILayout.EndVertical();
    }
    
    
    void ConvertAtlasImage()
    {        
        EditorGUILayout.BeginHorizontal();

        EditorGUILayout.EndHorizontal();

        EditorGUILayout.BeginVertical("box", GUILayout.Width(UIWidth));

        if(GUILayout.Button("선택된 메터리얼을 Change Shader로 변환"))
        {
            ChangeShaderToTargetMaterial();
        }
        EditorGUILayout.EndVertical ();
    }


    void ChangeShaderToTargetMaterial()
    {
        foreach (string path in InputPaths)
        {
            AssetDatabase.ImportAsset(path);

            Material material = (Material)AssetDatabase.LoadMainAssetAtPath(path);


            if (material.shader == shader1)
            {
                material.shader = shader2;

                material.SetColor("_TintColor", tintColor);
            }         
        }
    }
}

반응형
반응형

*제작한 이펙트가 처음엔 잘 나오다가 시간이 지나면 알파값이 제대로 안빠지고 소팅도 안되는 문제가 발생하고 있습니다.  동시에 프레임도 떨어지고 있습니다. .(갤럭시s5, 넥서스5, G2 모두 발생)

 

 

원본 이펙트

 

 

빌드후 시간이 지나면서 문제가 발생하는 모습

 

 

여러가지 테스트를 거쳐 원인을 파악해 본 결과 파티클에 패너 함수가 적용되면서 값이 누적되면서 발생하는 문제로 추정됩니다.

파트클에도 움직임을 주고 쉐이더에도 움직임을 주면서 뭔가 문제를 발생하는거처럼 보입니다.

패너값을 0으로 놓고 파티클로만 움직임을 주고 빌드를 하면 문제가 발생하지 않습니다.




**이 문제의 해결방안~

이 문제점을 커스텀 UV를 사용하여 해결하였습니다.

기존에는 PANNER를 이미지에 바로 연결하여 UV애니메이션을 적용했는데, 모바일 빌드시에는 텍스처에 바로 연결하지 않고

커스텀 UV를 만들어 연결해줘야 느려지는 현상이 없어집니다.

반응형
반응형

참고 자료

[데브루키/141206 박민근] 유니티 최적화 테크닉 총정리

http://www.slideshare.net/agebreak/unite2015-47100325?related=1

 

유니티 그래픽 최적화, 어디까지 해봤니

http://www.slideshare.net/ozlael/graphics-opt-ndc?related=2

 

최적화 내용 간단 정리

** 가장 중요한 것은 병목 지점 파악~(메모리인지, 콜인지, CPU인지, 버텍스연산인지.. 아니면 다함께..)

** 리소스 아껴 쓰기

** 드로우콜 관리하기

** 모바일 사운드는 모노로 사용하기~

** 가비지 컬렉터 관리~~

** 스크립트 함수에 무거운 함수 쓰지 않기~

** 셰이더에 무거운 함수 사용 줄이기~

** 물리 사용은 최소로~

 

1. 프로그램 최적화

* 스크립트 최적화

--- 유니티 객체들을 변수에 저장하고, 캐싱해서 사용하는 것이 좋다.

--- FindObject 계열 함수들은 매우 느리다. (미리 찾아서 캐싱)

--- Update 함수 보다는  Coroutine을 활용한다.

--- 박싱과 언박싱은 부하가 큰 작업이다.

--- 나눗셈보다 곱셈이 몇십배 빠르다.

--- 삼각함수의 값은 상수로 저장하고, 사용하는 것이 좋다.

--- 문자열은 readonly 혹은 const 키워드를 사용하여, 가비지 컬렉션으로부터 벗어나도록 한다.

* 문제의 원인 : 가비지 컬렉터

--- Mono의 동적 메모리 관리 때문에, 메모리 해제를 위해 GC가 자동 호출 된다.

--- GC는 언제 일어날지 모른다.

--- 오브젝트(or 프리팹)의 동적생성과 해제는 부하가 크다.

--- 오브젝트 풀링 사용은 선택이 아닌 필수!

--- 문자열 병합은 StringBuilder

--- foreach 대신에 for문 사용 (foreach는 한번 돌때마다 24Byte의 쓰레기 메모리를 생성)

--- 태그 비교에는 compareTag() 사용

--- 데이터 타입에는 Class 대신 구조체 사용

--- 즉시 해체할 때는 Dispose 수동 호출

--- 임시 객체들을 만들어내는 API들을 조심하라.

* C++ <-> C# 오버헤드

--- 객체의 변경 사항을 캐싱

--- 컴포넌트 참조를 캐싱

--- 빈 콜백 함수는 제거

 

 

2. 그래픽 최적화

* 텍스쳐 최적화

--- 권장 압축 사용하기 : 아이폰 -> PVRTC   ,  안드로이드(Tegra) -> DXT , 안드로이드(Adreno) -> ATC , 안드로이드(공통) -> ETC1 / ETC2

--- 텍스쳐 사이즈는 무조건 2의 제곱이어야 한다. POT(Power of Two) 아닐 경우 무조건 POT로 강제 변환함

--- 텍스쳐는 묶어서 사용하는게 이득이다. 한 화면에 나오는 것끼리~ 같은 재질의 오브젝트끼리~ 알파가 있는것과 없는것끼리

--- 32bit 텍스쳐보다는 16bit를 큰 텍스쳐로 쓰는게 이득이다.

--- 모바일뷰에서 가장 최적화된 해상도를 찾는데 주력한다. 이거저거 다해봐도 개개의 소스를 절약하는 방식이 최고임.

* 메쉬 최적화

--- Import시에 언제나 "Optimize Mesh" 사용 - 버텍스 캐쉬를 최적화 해준다.

--- 언제나 Optimize Mesh Data 옵션을 사용한다. 사용하지 않는 버텍스 정보들을 줄여준다.

--- 사용하지 않는 버텍스 정보들을 줄여 준다.

* 드로우 콜

--- 적절한 DP 는 100이하를 추천, 보통 70~100 정도가 일반적

* CULLING

---- 각 Layer 별로 컬링 거리를 설정해 준다. 중요도가 낮은 오브젝트의 컬링커리를 짧게 설정한다.

---- 오클루젼 컬링을 활용하여 카메라 밖은 잘라내도록 한다.

* 오브젝트 통합

---- 성질이 동일한 오브젝트들은 하나의 메쉬와 재질을 사용하도록 통합

*Batch

---- 스태틱 배치와 다이내믹 배치의 적절한 조합

* 라이팅

---- 라이트맵과 라이트렌더 모드 활용

* OverDraw

--- 한 픽셀에 두 번 이상 그리게 되는 경우 OVERDRAW 문제가 발생한다.

--- 기본적으로 앞에서 뒤로 그린다. Depth testing으로 인해서 오버드로우를 방지한다. 알파 블렌딩이 있을경우 소팅 문제 발생

--- 반투명 오브젝트의 개수의 제한을 건다. 뒤에서 앞으로 그려야 함. 반투명 오브젝트 갯수가 늘어날수록 퍼포먼스 직결~!

 

 

3. 사운드 최적화

--- 모바일에서 스테레오 사용은 퍼포먼스에 큰 영향을 끼친다. -> 되도록이면 모두 92 kb, 모노로 인코딩

--- 사운드 파일을 임포트하면 디폴트로 3D 사운드로 설정 -> 2D 사운드로 변경

--- 압축 사운드(mp3. ogg) , 비압축 사운드(wav) 구별.    압축 사운드-> 순간적인 효과음, 이펙트등..      비압축사운드 -> 배경 음악 

 

 

4. 폰트 리소스 최적화

--- Packed Font - R,G,B,A channel에 각각 글자의 형태를 저장하는 방법. 메모리 용량을 1/4로 절약

--- 리소스 기타 - ResourceLoadAsync() 함수는 엄청 느리다.

 

5. 셰이더 최적화

--- 기본 셰이더를 사용할 경우, 모바일용 셰이더 사용 Movile -> VertexLit 가장 빠른 셰이더

--- pow, exp, log, cos, sin, tan 같은 수학 함수들은 고비용이다.

--- 텍스쳐 룩 업 테이블을 만들어서 사용하는 것도 좋다.

--- 알파 테스트 연산(discard)은 느리다. 무조건 최소로..

--- 라이트맵 활용은 필수.

* 실수 연산

--- float : 32bit -> 버텍스 변환에 사용, 아주 느린 성능 (픽셀 셰이더에서 사용은 피함)

--- Half : 16bit -> 텍스쳐 uv에 적합, 대략 2배 빠름

--- fixed : 10bit -> 컬러, 라이트 계산과 같은 고성능 연산에 적합, 대략 4배 빠름]

 

6. 물리 엔진 최적화

--- FixedUpdate()는 Updata와 별도로 주기적으로 불리며, 주로 물리 엔진 처리

--- Default는 0.02초, 게임에 따라 0.2초 정도로 수정해도 문제 없음

--- 물리 엔진이 적용되지 않는 오브젝트는 Static으로 설정

--- 리지드 바디가 없는 고정 충돌체를 움직이면, CPU 부하 발생 - 물리 월드 재구성 - 이럴경우 isKinematic 옵션 사용

--- Maximum Allowed timestep 조정하여 물리 계산을 건너뛸 수 있는 부분은 무조건 건너 뛴다.

--- EDIT -> Project Setting -> Physics 에서 Solver Iteration Count 조정. 높을수록 정교하므로 낮게 설정한다.

--- Sleep 조절 -> 리지드 바디의 속력이 설정된 값보다 작을 경우, 휴면상태에 들어감.

--- Physics.Sleep() 함수를 이용하면, 강제 휴면 상태로 만듬.

--- 래그돌 사용 최소화

--- 태그 대신 레이어 활용 - 물리처리에서 레이어가 훨씬 유리, 성능과 메모리에서 장점을 가진다.

--- 메쉬 콜리더는 절대 사용 금지.

--- 레이캐스와 Sphere Check 같은 충돌 감지 요소를 최소화

--- Tilemap Collision Mesh -- 2D 게임에서 타일맵의 Collison Mesh를 최적화 하라.

 

반응형
반응형

http://acegikmo.com/shaderforge/

CG 스크립트 없이 쉐이더 편집을 가능하게 해주는 플러그인

최근 교육 받고 있는데 언리얼 쉐이더와 맞먹을정도로 편하네요~

홈페이지를 가보면 쉐이더를 눈으로 쉽게 알수 있도록 구성해 놨습니다.

쉐이더 공부하기에 매우 좋은 사이트~!

반응형

+ Recent posts