The Magic Of Material Property Blocks

반응형

이 글은 번역글입니다. 

원문을 참고하시길 강력권장합니다.

원문 : http://thomasmountainborn.com/2016/05/25/materialpropertyblocks/


MaterialProperyBlocks과 관련 API인[PerRendererData]을 발견했는데 무척 유용하긴 한데 정보가 별로 없다.

그래서 관련 API들이 어떤일을 하는지, 어떻게 쓰는게 가장 좋은지를 빠르게 정리하기로 했다.

만약 아직 이들에 대해 잘 모른다면, 이들은 분명 유니티 스킬에 좋은 도구가 되어 줄것이다.

자, 이제 어떻게 사용되는지 한번 살펴보자.

한 오브젝트의 색상이 애니메이트 되야 한다고 가정해 보자.

앞에서 이야기 했던건 무시하고, 2500개의 오브젝트가 색상이 애니메이트 되야 한다고 가정해 보자.


마음속에 가장먼저 떠오른 것은 아래 내용을 것이다.

1
GetComponent<Renderer>().material.color = ...


간단하지 않은가? 단지 색상만 바뀌고 애니메이트 되지는 않는다.

문제는 셰이더에서 색상을 바꾸기 위해 유니티가 GPU에게 이 오브젝트는 다르게 그려질 거야 라고

말해야 하며, 유일한 방법은 머티리얼 인스턴스를 바꾸는 것이다.

이 때문에, 유니티는 renderer.material에 처음 접근할 때

(비록 그 렌더러가 이미 그 머티리얼을 사용하는 유일한 렌더러가 아닐지라도)

머티리얼의 카피본을 만든다.

이 카피본은 그 renderer. 이후에 사용된다.

(이 카피본은 자동으로 파괴되지 않는것에 주의하자. 자기가 만든 메모리 릭은 자기가 제거해야 한다)


이는 메모리를 소비하는 머터리얼 복사본이 많이 존재할 수 있음을 의미한다.

아래 그림은 모든 구체가 하나의 고정 머티리얼을 사용했을때 메모리 프로파일러 모습이다.

scene에 40개의 머티리얼이 존재하고 47 KB정도를 필요로 하는것에 주목하자.

40개의 머티리얼중 오직 하나만이 우리의 구체에 사용되며, 나머지는 

평면이나 스카이박스등에 사용되는 기본 머티리얼이다.


이제, renderer.material을 사용할 때 메모리 프로파일러의 내용을 보자.

예상한대로, 2500개의 머티리얼이 생성되었다.

6 MB정도는 기가바이트 단위의 세계에서 그다지 커 보이지 않지만, 꽤나 많이 증가했다.

하지만, 내 생각엔 단지 몇메가 바이트를 아끼기 위해 이 글을 읽으려 하지 않을 것이다.

핵심 이슈를 보자 : GPU에 머티리얼을 변경을 업로딩 하기 위해 상당한 CPU 오버헤드가 있다.

2500개의 머티리얼들이 renderer.material을 사용하여 애니메이팅 될 때 CPU 프로파일러를 보자.

소중한 15.19 밀리세컨 시간이 가버렸다!

이 시간들을 다시 돌려 다른 유용한 곳에 잘 사용하고 싶어할 거라고 것이다.

예를 들자면 90 FPS에 도달하는것이다.

이제 MaterialPropertyBlocks과 [PerRendererData]을 만나보자.

이들은 이전과 정확하게 같은 scene에 대한 프로파일러지만, renderer.material 대신에 앞서말한 기술을 사용한다.


보이는가?

같은 효과에 0.01 밀리세컨이 걸린다.

이게 바로 승리이지 않은가.

강조된 두꺼운 녹색 띠가 어떻게 사라졌는지 살펴보자.

또한 모든 구체가 사실 같은 머티리얼을 사용하여 메모리 사용량이 단일 고정 머티리얼을 

사용할 때의 상태로 돌아간 사실을 알수 있다.


이 기술의 가치를 확인 했으므로 이제 직접 구현해 보자.

우선, 커스텀 셰이더를 만들어야 한다.

만약 셰이더 작성에 익숙하지 않다고 해도 조바심 내거나 할 필요가 없다.

그냥 프로젝트 탭에서 "Create"메뉴로 "Standard Surface Shader"를 만들면 된다.

그리고 나서, _Color 프로퍼티 앞에 [PerRendererData]를 추가하자.

이 속성은 셰이더를 사용하는 모든 렌더러에 공유되는 대신

_Color 속성이 렌더러 별로 설정되는 방식으로 유니티에 셰이더를 컴파일 하도록 요청한다.

(이러면 셰이더 컴파일 시간이 많이 늘어나지 않을까?)

이 속성은 모든 셰이더 프로퍼티에 추가할 수 있다.

1
2
3
4
5
6
7
Properties
{
    [PerRendererData]_Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
}

에디터에서 이 셰이더를 사용하는 머티리얼을 만들어 메시에 적용시키자.

머티리얼 에디터에 색상값 속성이 없어진 걸 확인할 수 있으며, 렌더 프로퍼티는 

스크립트에 의해서만 변경될 수 있다.

그러면 어떻게 스크립트에서 렌더러 데이터를 변경하는지 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SphereWithMaterialPropertyBlock : MonoBehaviour
{
    public Color Color1, Color2;
    public float Speed = 1, Offset;
 
    private Renderer _renderer;
    private MaterialPropertyBlock _propBlock;
 
    void Awake()
    {
        _propBlock = new MaterialPropertyBlock();
        _renderer = GetComponent<Renderer>();
    }
 
    void Update()
    {
        // Get the current value of the material properties in the renderer.
        _renderer.GetPropertyBlock(_propBlock);
        // Assign our new value.
        _propBlock.SetColor("_Color", Color.Lerp(Color1, Color2, (Mathf.Sin(Time.time * Speed + Offset) + 1) / 2f));
        // Apply the edited values to the renderer.
        _renderer.SetPropertyBlock(_propBlock);
    }
}

중요한 코드는 Update()에 있다.

두번째 줄을보면 MaterialPropertyBlock에 셰이더 프로퍼티를 쓰기 위해 SetColor()을 사용하였다.

MaterialPropertyBlock 클래스는 키와 값을 가지는 일반적인 c# 클래스보다 특별한건 없다.

키는 셰이더 프로퍼티 이름이며 값은 무슨값이든 해당속성으로 원하는 값을 넣으면 된다.


renderer.SetPropertyBlock()을 사용하여 렌더러에 새로운 값을 적용할 수 있다.

모든 작업이 완료되었다.

하지만, 렌더러의 프로퍼티 블럭을 설정하면 그 렌더러의 프로퍼티 블럭으로 다른 데이터를

덮어쓴다는 것을 아는것이 중요하다.

만약 새 프로퍼티 블럭이 기존의 값들을 가지고 있지 않다면, 값은 재설정 될 것이다.

즉, 머티리얼 프로퍼티 블럭을 통해 렌더러에 영향을 주는 여러 스크립트가 있는 경우

서로 영향을 미치게 된다.

그러므로 항상 새 값으로 바꾸기 전에 현재 프로퍼티 값을 먼저 얻자.

이 부분에 대한 내용은 Update()의 첫번째 줄에 있다.

걱정하지 마라, 이는 GPU에서 검색하는 것이 아니고 일반 메모리에 있는 데이터일 뿐이다.


[PerRendererData]키워드가 없는 셰이더 속성에서도 renderer.SetPropertyBlock()을 사용할 수는 있지만

이렇게 하면 유니티가 내부적으로 새 머티리얼 인스턴스를 만든다.


마지막으로 가장 좋은 연습은 MaterialPropertyBlock 인스턴스에 대한 필드 레퍼런스를 유지하는 것이다.

이는 실제 재질로 나중에 Destroy 시켜야 하기 때문이 아니라(MaterialPropertyBlock는 일반적인 C#클래스 임을 기억해라)

가비지에 콜렉트되어야 하는 새로운 오브젝트를 매 프레임마다 힙에 생성하지 않기 위해서 이다.

이렇게 MaterialPropertyBLock 과 [PerRendererData]에 대해 전반적으로 알아봤다.

15.19 밀리초 에서 0.01초 줄어든다.

이 API를 잘 활용할 수 있길 바란다.

한가지 아쉬운 점이 하나 있는데 모든 개체가 동일한 머티리얼을 공유하더라도

MaterialPropertyBlocks는 동적 일괄 처리(dynamic batching)를 해 주지 않는다.


Reference Link

- unity, MaterialPropertyBlock

- Learn how to use MaterialPropertyBlocks and [PerRendererData] for great performance gains!

- unity, Renderer.Material

- unity, Renderer.SharedMaterial

- material과 shareMaterial, 그리고 Material Property Block

-

'Unity > Unity Study' 카테고리의 다른 글

unity에서 shader를 짤때..  (0) 2017.07.10
ARB_precision_hint_fastest  (0) 2017.07.10
RenderQueue  (0) 2017.06.21
There are inconsistent line endings  (2) 2017.06.20
assetbundle building, change file  (0) 2017.06.03
TAGS.

Comments