This post is an example of the process of combining Substance designer and Unity shading while learning the Substance designer for the first time in 2013.
During the month of March, I would like to share code, node sharing, etc. while writing a basic manual for each node, previewing characters using a light capture shader, and a tutorial (intermediate and advanced) that integrates a lightcapture shader with dynamic texture and parameter nodes.
Let’s try posting as much as possible rather than holding it too big.
Node-specific manuals will be written at the level of the example below.
Filter = node = library?
Nodes are, in simple terms, subgraphs of each subsegment, each functioning in their own segment.
You can find them all in the Library view, which is placed by default in the bottom left of the Substance Designer screen.
It is easily written as a node, but it is a library of substances used for that node.
It would be better not to write top-down, but to talk about the most accessible library and to reinforce it by discussing opinions about simple application approaches.
Substance Online Manual stipulates that each function in the library is a filter.
However, in the library, individual functions, meshes, sbs, and sbsr are all included in addition to filters.
Substances (graphs from .sbs package and .sbsar) Bitmap images Vector images Functions Meshes
The above list is a detailed category that can be accessed within the library.
It is good to organize the filters for each filter while making an easy-to-understand Case by sample graph.
The easiest way to understand substance is that the graph concept is almost the same as non-linear editing software such as kanata, fusion, etc., used in movie shorts.

3월 작성 해 볼 라이트켑와 서브스턴스 디자이너 연동의 예상 결과물입니다. 도타2 의 영웅 모드 데이터를 이용 하여 설명 합니다.
연구목표는 아래와 같습니다.
Color Adjust function target list.
HSV 변조 매개변수(Expose a parameter)
Contrast 매개변수
Dynamic texture generating for ligthCap(Matcap bake texture based) shader.
GLSL shader code writing for Substance designer 3D View. 최종 통합 되어 구현 될 유니티에서 사용 할 ” Lightcap shader ” 와 거의 같은 쉐이딩을 서브스턴스에서도 직접 구현 해 봅니다.
간략한 이해를 돕기 위해 서브스턴스의 ” 내부 쉐이더 라이브러리의 구조를 참조 ” 합니다.
위 모든 서브스턴스의 필터를 통한 매개변수는 쉐이더 안에서도 작성 할 수 있습니다. 연구의 목표는 연속적인 프로세싱을 최소화 하는 Look up texture table 을 미리 작성 된 서브스턴스의 매개변수로 부터 다이나믹 텍스처 과정을 거처 static computing 화 하는 것에 있다고 볼 수 있습니다.
Substance power user 에게는 불필요 한 내용 일 수 있지만 앞으로 Substance power user 가 되실 분들을 위해 손풀기로 노드를 살짝? 다루어 보면서…
아~~~ 대략 이런 것이구나 하는 내용 잠깐 보고 갈께요?! 느낌 아니까~~~
보통 프로그래밍을 하다 보면 API 를 사용 하는 경우가 많습니다. 다만 그 API 를 어떤 식으로 어디에 사용 하는지에 따라 다른 경우 또는 같은 경우 이지만 확장성이 편리 하거나 유지보수를 더 쉽게 할 수 있거나 하는 식으로 프로그래밍 구조가 짜여 지게 되겠죠. 역시 이것은 사람의 성향에 따라 달라지게 되는 것입니다.
Substance 의 normal output node 를 보죠.
아래 그림처럼 회색 이미지를 통해 내부적으로 작성 되어 있는 Normal mapper 필터를 경유 함으로서 자동 변환 됩니다.

하지만 무언가 우리가 생각 하고 있는 것과는 매치가 잘 되지 않는군요?! 단순히 Normal mapper 내부적으로 갖고 있는 파라메터에는 한계가 있어 보이는 군요.!
이제 한단계 더 나아가 볼까요?
좌측 하단의 라이브러리 창에서 검색어 입력 라인에 normal 이라고 검색어를 넣습니다. 아직 숙련 되지 않았다면 필더를 찾는것은 꽤나 어렵게 느껴 질 겁니다. 하지만 Substance designer 의 개발자들이 이름을 잘 지어 놨군요! ^^

검색 된 리스트 에서 Normal Color 를 선태 하고 Graph 창에 드레그 해서 가져 옵니다.
위에서 사용 된 입력 페턴의 가장 검정색 부분에 대해서 생각 해 보죠.
가장 깊은 곳입니다.
이것을 Height 맵으로 이해 한다면 좋겠네요.
노말텍스처는 세개의 컬러 체널을 사용 하여 좌표를 기록 하고 있습니다. R = 좌우 G = 상하 B = 깊이
대략 이렇게 이해 했다면…. Normal mapper 로 생성 된 노말맵의 Z 값을 변조 해야 합니다. Normal Color 내부 파라메터인 Slope 을 이용 하여 Blue 값에 가깝도록 변형 해 보세요.

결과를 위해 Blender 를 사용 하여 합쳐줍니다.
주의 할 것은 여기서는 Normal Combine 을 사용 하지 않습니다.
위의 예제에서 처럼 프로시주얼 페턴의 블랙 부분은 알파 영역으로 취급 되어 Normal Mapper 노드를 경유 하면서 완전히 오퍼시티 영역으로 규정이 되는 것이고 내부적으로 투명 영역은 화이트 처럼 인식 해 버릴지도 모릅니다. 그렇기 때문에 Blender 를 통해서 빈 공간을 채워 줄 노말 칼라를 만들어 주어야 하는 것이고 결과를 위해서 두 소스를 그대로 합쳐 주기만 할 것이기 때문이죠.

이제 어느정도 예상 했던 결과물이 되었죠? Slope 각도에 따라 적당히 제어를 하면서 원하는 결과를 만들어 낼 수 있겠습니다.
결론. 서브스턴스는 하나의 길을 제공 하는 것이 아니라 노드의 결합을 이용 한 무한한 확장이 가능 한 차세대 비선형 동적 텍스처 생성 프로그램 이라고 이해 하시면 좋겠습니다. 수많은 노드 연결 학습으로 여러가지 결과물을 만들어 보는 것이 가장 좋은 학습이겠죠!
서브스턴스 디자이너에서는 몇가지 노말맵 생성기능을 제공 하고 있습니다.
PART-2 에서는 보편적인 Normal Mapper 필터를 잠깐 살펴 봤는데요. PART-3 에서는 Height Normal Blender 필터를 경유 하여 노말맵을 생성 하였습니다.
PART-3 의 목표는 서브스턴스에서 기본적으로 제공 하는 렌더링 엔진의 노말맵 연산에 사용 되는 Binormal 과 UV Space 간의 계산을 위한 노말맵의 녹색 체널에 대한 이야기를 살짝 다루면서 서브스턴스의 맥락과 이해에 좀 더 접근 해 보는 것에 두었습니다.
쉽게 말하면 원래 서브스턴스에서 내부적으로 제공 하는 DirectX 모드 와 OpenGL 모드에 따른 Y 체널 Invert Function 을 사용자 노드로 다시 구현 해 보는 것이죠.
이번 구현에서는 쉬운 이해를 위한 방법을 선택 하고 있고 좀 더 어려운 매개변수 개방을 위한 내부 함수(Internal built-in Function)로 변수 개방을 하지 않습니다.
이해를 위해서 간단한 노드를 구성 했어요.

그냥 딱 봐도 정상적으로 출력 되고 있어 보입니다.
테스트를 위해서 일단 렌더링 엔진을 확인 해 봐야겠습니다.

예문 작성을 맥에서 하고 있기 때문에 OpenGL GLSL 로 설정을 하였습니다.
SSE2 는 인텔 CPU 에서 제공 하는 가속 렌더링 아zl텍처 입니다. 서브스턴스는 내부적으로 Bool 타입을 green channel inverting function 을 제공 하고 있습니다.
Normal Format 을 보면 다이렉트 엑스와 오픈지엘을 선택 해 줄 수 있어요.
하지만 어차피 개념적인 이해를 위한 예제 이기 때문에 이것은 생각 하지 않겠습니다.
우리는 노멀맵이 세개의 체널로 부터 작성 되고 있다는 것을 알고 있습니다. 즉 녹색 체널을 반전 시켜야 하죠.


노드를 좀 더 활용 해 보기 위해서 channel split 으로 RGBA 체널을 분리 시키고 다른 필터를 경유 시킨 후에 Channel merge 하여 병합 후 Output 노드로 연결 하겠습니다. 무엇을 해야 할까요? 녹색 체널을 반전 하기 위해서 분리 노드와 병합 노드 사이의 G체널에 Invert 와 관련 된 필터를 연결 해 보는 것이죠.
역시 좌측하든의 라이브러리 창에서 invert 로 검색 해 보는 겁니다.

우리는 회색 체널을 반전 해야 하기 때문에 Invert Grayscale 필터를 사용 할 것입니다.

Invert Grayscale 필터를 연결 해 보니 노말맵의 위아래가 바뀐 결과를 볼 수 있어요.
왜 이런 체널 인버트를 해야 하는지는 Binormal 연산이 탄젠트 공간과 UV 공간 좌표계의 계산에 의해 이루어 진다는 겁니다.
다이렉트 엑스는 좌하단이 0.0 이고 오픈지엘은 좌상단이 0.0 의 UV 공간 좌표계를 사용 합니다.
쉽게 생각 하면 위아래가 뒤집혀 있다는 거죠.
렌더링 아키텍처에 따른 올바른 법선맵핑 연산을 위해서 가볍게 … 뭐 아.. 그렇군. 정도로 이해 하시면 될거 같습니다.
보통 쉐이더 작성시에는 Y 값에 대해서 마이너스 값으로 치환 대입 하여 처리 하고는 하죠.
하지만 쉐이더에서는 지속적인 계산을 수행 해야 하기 때문에 중간 작업용 UberShader 등에서는 이런 Bool 타입 파라메터가 있고 if 문 등으로 처리 할 수 있지만 연산은 그다지 달갑지 않은 탓에 다이나믹 텍스처를 통해 초기 한번만 처리 하고 결과를 리턴 할 수 있도록 하는 것이 좋을 듯 합니다.
가볍게 노드란 이런 것이구나 살펴 봤다면 이제 substance designer 의 filter 와 custom shader 를 추가 하면서 간단한 mobile shading 을 해 볼 것입니다.
first step 으로 저는 Lookup texture based Rimlighting texture 를 만들어 볼겁니다.
이것은 포토샵에서 간단하게 만들 수도 있지만 좀 더 substance designer 의 filter 와 Dynamic texture 의 강력한 기능 그리고 매개변수를 노출 하면서 Unity 에서 어떻게 B2M 과 같이 사용 할 수 있는 것인가를 이해 하기 위해 다양한 방법으로 접근 해 볼 것입니다.
먼저 제작 목표가 될 Lookup texture based Rim lighting texture 입니다

위에서 보여지는 텍스처는 링크에서 보시는 것과 같은 Rim Lighting 의 Pre calculated lookup texture 로 사용 됩니다.
lookup texture 를 사용 하는 이유는 모바일 그래픽의 경우 Pow. function ( 거듭제곱 ) 을 자주 사용 하는 것은 섬세하지만 연산에 있어서 Cost 가 무척 높기 때문이죠.
참고로 sqrt 는 제곱근 입니다.사실 Pow function 의 내부를 들여다보면 꽤나 복잡한? 연산을 수행 하고 있습니다.
Pow function 에서의 옵티마이징에는 여러 방법이 있습니다. Pow function 알고리즘을 직접 구현 할 때 int 타입으로 만들고 거듭제곱의 테이블을 미리 정의 하는 것이죠. 다만 우리가 사용하는 쉐이더 컴파일러에서 제공 하는 Pow function 은 none int 타입입니다. 보통 Specular exponent 등을 구현 할 때 사용 하죠. 이것은 여러 방법으로 최적화 할 수 있지만 대략 그런 것이 있구나 정도만 언급 하도록 하죠.
아무튼…
위와 같은 이미지를 얻어 내기 위해서 substance designer 의 어떤 기능을 활용 할 것인지… 그것이 중요 하겠죠.
하지만 여기서 가장 중요 한 부분은 SVG 가 아닙니다.
어떻게 하얀색 튜브를 만들어 낼 것이냐 이죠.
이제 라이브러리를 좀 살펴 볼께요.
흐음…
검색 하다 보니 맘에 드는 것이 나타 납니다.

바로 이것 입니다.
해당 필터에는 여러가지 옵션이 붙어 있습니다.
substance designer 의 개발자들이 이 필터를 그냥 만들진 않았을 것이라는 희망을 갖고 시작 하게 되었죠.
사실 이런 방식으로 제작 하지 않아도 됩니다. 기초 예제 라서 다른 여러가지 노드를 사용 하는 방식으로 제작 해 봤습니다.

Splatter Circular 를 Graph 에 Drag 해서 하나 만들어 보면 Number 라는 파라메터가 있습니다.
기타 Radius 에 대한 옵션도 있죠.
포인트는 Number 에 있습니다.
시각적으로 크게 어색하지 않는 수 만큼을Type 입력 하면 위와 같이 Circle tube 가 만들어 집니다.
좋습니다.
이제 가운데 부분을 부드럽게 Blur 링 파라메터를 추가 하고 싶으실 겁니다.
그리고 이것은 Dynamic Texture 형태로 유지 되어야 하고요.
단순히 Bitmap 으로 뽑아서 사용 하려고 이렇게 어렵게 substance designer 를 사용 하는 것은 아닐겁니다. 🙂

SVG node 를 하나 만들고 Shape tool 을 사용 하여 그림처럼 Black circle 을 만들어 줍니다.
이건 뭐에 쓸까요?
네… 저는 첨에 만들었던 Splatter Circle 의 중심에 SVG 로 만든 노드를 Blending 할 생각 입니다.

Rim light texture 를 만들때 안쪽은 Blur 를 줘서 부드럽게 사라지는 느낌을 주기 위해서 Blur HQ 노드를 중간에 연결 했습니다.
그리고 이것을 Blending node 에서 알맞게 사용 할 수 있도록 Grayscale 로 바꾸어 줬죠.

중요한 포인트 인데… SVG 를 바로 Blur HQ 노드로 연결 후 Blending 의 Foreground Layer 로 사용 하면 원하는 결과를 얻을 수 없습니다.
SVG 의 투명 속성에 대해서 Flatten Alpha 를 True 로 변경 해 줘야 하기 때문이죠. 이것은 Blending node 의 특성과도 관계가 있는 것이겠군요.

결과를 위해서 연결 된 Node 입니다.
Blend 노드의 확대 이미지 입니다.

Multiply 옵션을 사용 하여 Splatter circle 과 SVG 가 합성 된 것을 알 수 있을 겁니다.

위 그림은 이번 장에서 작성 한 Graph 를 Instance 화 하여 다른 Graph 에서 제활용 하는 구조입니다. 서브스턴스를 잘 활용 하려면 instance 에 대한 개념을 잘 알아 두는 것이 좋습니다. 다음 파트 에서는 Blur HQ 값 , 또는 Blending 값을 활용 하여 매개변수로 노출 시키고 Rim light texture 를 Instance 를 사용하고 변수노출을 하여 유니티 에서 Dynamic 하게 활용 해 보는 것을 살펴 보겠습니다.

작업을 위해 신규 도큐먼트를 만들어 보세요. 이때 Template 에서 세 번째 있는 Physically Based(metallic/roughness) 를 선택 해 봅니다. 아직 우리는 커스텀 쉐이더를 만들어 추가 한 상태가 아닙니다.
When you are creating new substance project and you would be selecting 3th category of template. So, currently We did not added to custom shader for this project.

Dota2 mode 에서 추출 한 Bounty Hunter 3d model 을 drag 하여 unsaved package 에 올려 놓으면 위 그림처럼 자동으로 Resources folder 가 만들어 지고 mesh data 가 등록 됩니다.
Dragging extracted Bounty Hunter 3d modeling data and then while you hovering mouse cursor to your unsaved package or your custom sbs project. You will be see automatic creation resource folder like above images.

등록 된 상태에서 모델 아이콘을 더블클릭 하면 3D View 창에 자동으로 해당 모델이 나타나게 되죠. 이때 외부에서 모델을 수정 하고 저장 하면 substance designer 에서는 자동으로 업데이트 시켜 줍니다.
If your 3d modeling data to finished registration and then next you can just double clicking your 3d modeling that just done. ta da~! While you guys if edit your 3d modeling via external DCC tool and directly step you will see automatic updating substance designer.

미리 만들어져 있는 기본 텍스처 페스들을 리소스 폴더에 드레그 해서 등록 합니다.
만약 여러분이 특정 폴더 안의 리소스 경로를 유지 하고 싶다면 link , 서브스턴스 디자이너 전용 프로젝트 폴더를 따로 관리 하신다면 import 로 설정 하면 되겠습니다. 사실 임베디드 데이터가 되는 것은 아니고 복사가 자동으로 되는 것일 뿐입니다. 위 그림에는 도타2 에서 추출 한 여러 텍스처 페스 소스가 있군요. 이번엔 학습을 위해 모두 사용하지는 않을 겁니다.
참고사항으로 이번 학습예제는 Valve soft 에서 제공하는 도타2 아트웍 매뉴얼과는 상관 없는 비주얼 프로세스를 다루고 있습니다.이제 기본적인 준비는 끝이 되었습니다. 다음으로 요구 되는 텍스처 아웃풋을 만들어 주고 목표 한 작업을 위해 Graph 에서의 노드 작업을 해 보죠. 디퓨즈 , 노말 , AO TEX , MATCAP TEX , RIM TEX. 이렇게 5개의 텍스처 소스가 필요 합니다. 5개의 텍스처 소스 중에서 이미 확보 되어 있는 것은 디퓨즈 , 노말 , MATCAP TEX 이렇게 3개 이고 substance designer 에서 직접 만들어야 하는 것이 나머지 두 개 입니다.
Doing just dragging your pre made pass textures into your current .sbs project root. If you want to preserve your other directory path , have to choose just link mode or other case you want to preserve include currently sbs own project directory you have to choose import mode. So, import mode is just copy into sbs project location that is not embedded data. Include many other kind of pass texture see above images. This chapter is not implementation about Valve softs’ dota2 graphics manuel.So.Now we ready to basically assets for next work. And then We will have to making texture for each working in require output graph node. This element tex is diffuse , normal , AO TEX , MATCAP TEX , RIM TEX that is done. Diffuse , Normal and MATCAP TEX is already that made pre calculating external tool and You will have to making just another texture only.

먼저 AO TEX. 를 만들어 볼텐데요 여기서 이제 substance designer 의 인스턴스 라이브러리를 사용 하게 됩니다.
First time We will making AO TEX that now you will utilize instance library in substance designer.

이렇게 노말맵과 substance designer 의 Normal to Height 과 Ambient Occlusion 이라는 Instance filter 를 활용 하면 간단하게 AO 맵을 만들 수 있는 공정이 됩니다.
So… Normal to Height filter and Ambient Occlusion filter on substance designer by normal map textures at like this technique to simply you make for your AO TEX that is too some easy process I am sure if you has to understanding precess about many other filter to substance designer.
하이폴리곤 디테일 모델링이 없기 때문에 노말맵과 내부 필터를 사용하여 AO TEX 를 만들게 되지만 Zbrush 등에서 하이폴리곤 작업을 했다면 Bake model information 에서 AO TEX 를 만들 수도 있습니다.
In this case just utilized normal map and built in instance filter for making AO TEX because we does’t has original high polygon model source but if we have that source we can utilizing Bake model information on substance designer.

적당한 값을 설정 하여 원하는 AO TEX 를 간편 하게 얻어 낼 수 있습니다.
You would be get some fitting AO TEX texture result for your work.

그림은 기본 노드를 사용 한 AO TEX. 다만 목표 쉐이딩을 위해 한단계 가공을 해 보겠습니다. 중간톤 부분의 디테일이 좀 더 필요 한 이유로 Highpass 를 사용 한 별도의 노드와 블렌딩을 거쳐 사용자화 텍스처로 변환 해 봅시다.
Detail AO 프레임을 보면 Height map 을 사용 하여 Hipass 로 필터링 한 후 블렌딩을 위해 그레이스케일 컨터빙을 한 출력값과 AO TEX 프레임에 있는 Ambient Occulusion 의 출력값을 혼합 , Level 을 통해 적정한 값을 유추 하였습니다.

약간의 값 변화를 주어 1차 결과물을 추출했습니다.
다만 마지막 쉐이딩 에서의 결과물을 확인 하는 시점에서 사용자 AO TEX 의 값은 달라 질 수 있습니다. (AO 의 원론적인 출력 결과로 볼때는 올바르지 않을 수 있습니다 , level 등과 이 전의 노드의 속성을 달리 해 보면서 유추 되어 지는 결과를 자주 보는 것이 substance designer 의 노드 그래프 속성에 대해서 빨리 이해 하는 길입니다.)

Substance designer 에서 node 를 사용하여 작성 된 AO TEX <-(Detailed AO) 를 모델의 Shader node 의 Ambient Occlusion 에 연결 하였습니다.

Normal texture 도 조금 보강을 합니다. 다만 이것은 학습을 위한 것일 뿐입니다.
미리 작성 되어 있는 Normal Texture 와 Diffuse texture 를 이용하여 변환 된 detail normal texture pass 을 Normal Combine node 를 활용 하여 섞어 줍니다.
Detail normal texture는 contrast/luminosity_grayscale 을 경유 하도록 하였는데요 그레이톤으로 변경 된 diffuse texture 를 적당한 normal texture 로 변경 한 뒤에도 handling 할 수 있도록 한 것입니다.
contrast/luminosity_grayscale 역시 Instance 인데요…여기서 매개변수를 노출 시켜 게임상에서 디테일 노말을 제어 할 수도 있겠습니다.

참고사항. Physically Based(metallic/roughness) 를 사용 할 때 metallic 값을 변경 하는 체널은 R 체널 입니다. 위 그림은 이해를 돕기 위한 것인데요. uniform color 의 RGB slider 를 움직여 보면 알 수 있습니다.
즉 metallic power 등을 R 체널에 비트맵으로 삽입 해 주어도 되겠지요?

유니폼 컬러로 일괄적으로 metallic power 가 반영 된 예.

살짝 삼천포로 빠진 것 같긴 하지만 살짝 이해를 돕기 위해서 비트맵을 이용해서 시각적으로 노드를 구성 해 봤습니다. ( 그림을 클릭 하면 엄청 크게 나옵니다. -_-;)
중간에 level node 를 두고 조절 해 보면 Metallic power 가 어떤 식으로 반영 되는지 더 시각적으로 확인 해 볼 수 있죠. 역시 이런 식으로 매개변수를 노출 시켜서 게임에서 활용 하는 것입니다. 대략 이런식으로 PBS 에서는 작업 한다는 정도 인데요. 3월 연구 목표의 범주가 커스텀 쉐이더의 적용과 룩업텍스처의 제작 이니까…
슬쩍 이렇게 넘어가고 다음 쳅터에서는 GLSL 부분도 조금 씩 살펴 보도록 하겠습니다. 회사에서는 자체엔진을 사용 하기 때문에 PC 에서는 cg 와 모바일에서는 gles . 이렇게 코드 짤 때 두개를 다 함께 짜야 해서 정말 피곤이 절정에 이르는데요…

일단 여기까지 꼭지만 일단 ….
이번 쳅터는 오우거 엔진을 좀 보셨던 분은 아…. 대충 비슷 하구나. 란 느낌이 들거 같네요.
파싱 콘테이너는 xml 로 되어 있고 이것은 오우거 쉐이더 파싱 구조와 유사 합니다.
오우거 엔진에서 사용 되는 .pragram 과 거의 비슷 합니다.
오우거 엔진 에서는 .pragram 에서 matrix semantic definition 이나 uniform variable value definition 등을 define 해 줘야 하거든요.
타입은 xml 인데 쉽게 이해 되실 듯 합니다.
일단 matcap 쉐이더 에서는 라이트에 관한 구현부가 전혀 없기 때문에 주석처리.;

주말 출근 시즌이라 이런 저런 일을 처리 하고 좀 더 추가 해 봅니다.
정말 쥐 오줌처럼 조금씩 추가 하게 되네요. 🙁
요즘 회사에서 FumeFX 와 Particle Flow 와 씨름 중이라 머리가 폭팔 할 지경 입니다.
사족이 길었군요… ㅋ; 왠지 내용이 길어 보이고 좋군요. -_-;
좀 귀찮기도 하고 해서 커스텀 쉐이딩 쪽은 구글링을 하지 않고 이리 저리 컴파일 해 보면서 테스트 중이랍니다. 일단은 서브스턴스 메뉴얼에 커스텀 쉐이더 추가 규칙이나 내부 API 문서가 있는 거 같진 않은데 안찾아 봐서 모르겠습니다. 누가 좀 찾으시면 링크 좀…. 찾았어요.. -_-;
서브스턴스 디자이너 내부에서 이미 정해 져 있는 Output 타입을 살펴 보면 ambient 가 있습니다. 결국 matcap 은 반구형의 2D 암비언트 텍스처 맵이라고 볼 수 있기 때문에 ambient 아웃풋 노드를 만들도록 합니다.
그리고 서브스턴스 디자이너를 위한 glslfx 안에서 선언 해야 하는 usage 키워드와 서브스턴스 디자이너 코어 라이브러리와 에디터 라이브러리 내부에서 구현 된 대리자(아마도…)넘겨 주는 지시자 역시 미리 정의 되어 있는 범주 안에서만 커스터마이징이 가능 한 것으로 보입니다.(일단 여기까지는 제가 대략 추정 한 내용 일 뿐입니다.)
좀 더 진행을 해 보기 전에 몇 가지 확인 하고 넘어 갈 생각인데요. 쉐이더나 glslfx 파일을 수정 후 다시 컴파일 하려면 3D View 를 클릭 한 상태에서 Ctrl + R 을 누르면 제컴파일이 됩니다.

만약 잘못 된 라인이 있다면 그림 처럼 로그를 보여 줍니다.
이렇게 말이죠. 암튼… 전체적인 구성 자체가 왠지 저는 오우거 엔진 기반 같다는 생각이… 떠나질 않네요. ^^
glslfx 에서의 usage 는 확실히 미리 코어 라이브러리에서 정의 되어 있는 키워드 같습니다. 만약 아래와 같이..
<sampler name=”ambientHemisphericalMap” usage=”ambient”/>
가 아닌
<sampler name=”ambientHemisphericalMap” usage=”ambientMap”/>
디파인 된 키워드를 사용 하면 인터페이스에는 sampler name 을 표시 하도록 되어 있네요.
솔직히 코드소스 보면 금방 알텐데… ㅜㅜ; 5분도 안걸리는 건데…. 이틀을 삽질을.;;; 암튼…
http://support.allegorithmic.com/documentation/display/SD41/GLSLFX+Shaders?src=contextnavpagetreemode ( GLSLFX 레퍼런스 )

정의 되지 않는 usage 키워드를 사용 하면 sampler 이름을 문자열로 리턴 해서 사용 하도록 되어 있습니다. 마단 실제 usage 키워드가 정해진 대로만 사용 해야 하는 지는 좀 더 실제 쉐이더 코드를 만들어 보면서 살펴 볼게요.
솔직히 이거 쓰면서…. ㅜㅜ; 쉐이더는 아무것도 아닌데…
XML 데이터의 정확한 규칙을 모르겠음. 이건 뭐 … 코어 인테페이스 로직에서 뭔 짓을 해 놨는지 알아야..;;; -_-;
암튼… 몇일동안 계속 삽질 중입니다.

여차 저차 하여 기본적인 디퓨즈 컬러와 암비언트 맵이 적용 되도록 쉐이더 코드를 수정 하였습니다.

차 후에 matcap + 2 lighting 을 해보려고… 3dsmax 2015 의 shaderFX 에서 matcap 을 간단히 구현 해 봤습니다

일단 코드 테스트를 위해서 그림처럼 RGB 텍스처를 만들도록 합니다.

위와 같은 결과를 얻을 수 있도록 쉐이더 코드를 작성 할 것입니다. 카메라를 돌려도 계속 색의 패턴이 프로젝션 맵핑 되는 것 처럼 보인다면 대략 완성 단계에 가깝다고 보시면 됩니다.
matcap.glslfx 코드.
<?xml version="1.0" encoding="UTF-8"?>
<glslfx version="1.0.0" author="allegorithmic.com">
<!-- TECHNIQUES -->
<technique name="Matcap">
<!-- PROPERTIES -->
<property name="blend_enabled" value="true"/>
<property name="blend_func" value="src_alpha,one_minus_src_alpha"/>
<property name="cull_face_enabled" value="true"/>
<property name="cull_face_mode" value="back"/>
<!-- SHADERS -->
<shader type="vertex" filename="matcap/vs.glsl"/>
<shader type="fragment" filename="matcap/fs.glsl"/>
</technique>
<!-- INPUT VERTEX FORMAT -->
<vertexformat name="iVS_Position" semantic="position"/>
<vertexformat name="iVS_Normal" semantic="normal"/>
<vertexformat name="iVS_UV" semantic="texcoord0"/>
<vertexformat name="iVS_Tangent" semantic="tangent0"/>
<vertexformat name="iVS_Binormal" semantic="binormal0"/>
<!-- SAMPLERS -->
<sampler name="diffuseMap" usage="diffuse"/>
<sampler name="normalMap" usage="normal"/>
<sampler name="matcapMap" usage="emissive"/>
<sampler name="AOMap" usage="ambientocclusion"/>
<sampler name="rimMap" usage="mask"/>
<!-- MATRICES -->
<uniform name="modelViewMatrix" semantic="modelview"/>
<uniform name="worldMatrix" semantic="world"/>
<uniform name="worldViewMatrix" semantic="worldview"/>
<uniform name="projectionMatrix" semantic="projection"/>
<uniform name="worldViewProjMatrix" semantic="worldviewprojection"/>
<uniform name="worldInverseTransposeMatrix" semantic="worldinversetranspose"/>
<uniform name="viewInverseMatrix" semantic="viewinverse"/>
<!-- SCENE PARAMETERS -->
<uniform name="ambientColor" semantic="ambient"/>
<!-- UNIFORMS -->
<uniform name="tiling" guiName="Tiling" default="1" min="1" guiWidget="slider" guiMax="10"/>
<uniform name="heightMapScale" guiGroup="Normal" guiName="Normal Strength" default="1" guiWidget="slider" guiMin="0" guiMax="2" />
<uniform name="matcapPowr" guiGroup="MatCap" guiName="Matcap Strength" default="1" guiWidget="slider" guiMin="0" guiMax="5" />
<uniform name="flipY" guiGroup="Normal" guiName="DirectX Normal" default="true" guiWidget="checkbox" />
</glslfx>
vs.glsl 코드
/////////////////////////////// Vertex shader
#version 120
attribute vec4 iVS_Position;
attribute vec4 iVS_Normal;
attribute vec2 iVS_UV;
attribute vec4 iVS_Tangent;
attribute vec4 iVS_Binormal;
varying vec3 iFS_Normal;
varying vec2 iFS_UV;
varying vec3 TtoV0;
varying vec3 TtoV1;
varying vec3 TtoV2;
varying vec4 iFS_Tangent;
varying vec4 iFS_Binormal;
varying vec4 iFS_PointWS;
uniform mat4 modelViewMatrix;
uniform mat4 worldMatrix;
uniform mat4 worldViewMatrix;
uniform mat4 worldViewProjMatrix;
uniform mat4 worldInverseTransposeMatrix;
uniform mat4 viewInverseMatrix;
uniform mat4 projectionMatrix;
void main()
{
gl_Position = worldViewProjMatrix * iVS_Position;
iFS_UV = iVS_UV;
iFS_Normal = (worldInverseTransposeMatrix * iVS_Normal).xyz;
iFS_Tangent.xyz = (worldInverseTransposeMatrix * iVS_Tangent).xyz;
iFS_Binormal.xyz = (worldInverseTransposeMatrix * iVS_Binormal).xyz;
vec3 biNorm = cross( iFS_Tangent.xyz, iFS_Normal ) * iFS_Tangent.w;
iFS_PointWS.xyz = (worldMatrix * iVS_Position).xyz;
mat3 rotationMat = mat3(iFS_Tangent.xyz , biNorm , iFS_Normal);
// use like unity3d macro based matrix function name
mat3 UNITY_MATRIX_IT_MV = mat3(worldViewMatrix * worldInverseTransposeMatrix ); // inverse transpose matrix for Inverse transpose model view matrix
mat3 TtoV_Mat = UNITY_MATRIX_IT_MV * (rotationMat);
TtoV0 = TtoV_Mat[0];
TtoV1 = TtoV_Mat[1];
TtoV2 = TtoV_Mat[2];
}
//vs shader end
-------------------------------------------------------------------------------
버택스 쉐이더에서 유심히 볼 것은 ...
제가 잘 몰라서 그런지 ... 암튼...
서브스턴스 디자이너의 빌트인 메티릭스 에
Inverse Transpose modelview matix 가 없던 건데요...
보통 어지간한 엔진들은 내부에서 역전치 행렬 공간변환에 대한 것도 정의가 다 되어있는데 말이죠....
아무튼...
mat3 UNITY_MATRIX_IT_MV = mat3(worldViewMatrix * worldInverseTransposeMatrix );
식으로 공간변환을 추가 해 줬습니다.
UNITY_MATRIX_IT_MV 는 유니티 유저이기 때문에 머리에 빨리 들어 오도록 이름을 같게 했습니다.
rotationMat 는 Tangent rotate Transpose 입니다.
유니티 쉐이더랩 에서는 메크로 화 되어 있는데 풀어서 쓰면
mat3 rotationMat = mat3(iFS_Tangent.xyz , biNorm , iFS_Normal);
가 됩니다.
하지만 iFS_Binormal 을 사용 하지 않고 따로 외각을 구하는 cross 함수를 사용 해서
따로 biNorm 을 만들어 주어야 합니다.
vec3 biNorm = cross( iFS_Tangent.xyz, iFS_Normal ) * iFS_Tangent.w;
이렇게 말이죠.
유니티 쉐이더랩에서 메크로화 되어 있는
Transformations 참조.
https://docs.unity3d.com/Documentation/Components/SL-BuiltinValues.html
-------------------------------------------------------------------------------
fs.glsl 코드
//////////////////////////////// Fragment shader
#version 120
varying vec3 TtoV0;
varying vec3 TtoV1;
varying vec3 TtoV2;
varying vec2 iFS_UV;
varying vec3 iFS_Normal;
varying vec3 iFS_Tangent;
varying vec3 iFS_Binormal;
varying vec3 iFS_PointWS;
uniform vec3 Lamp0Pos = vec3(0.0,0.0,70.0);
uniform vec3 Lamp0Color = vec3(1.0,1.0,1.0);
uniform float Lamp0Intensity = 1.0;
uniform vec3 Lamp1Pos = vec3(70.0,0.0,0.0);
uniform vec3 Lamp1Color = vec3(0.198,0.198,0.198);
uniform float Lamp1Intensity = 1.0;
uniform float Ka = 1;
uniform float heightMapScale = 1;
uniform vec3 ambientColor = vec3(0.07,0.07,0.07);
uniform float tiling = 1.0;
uniform bool flipY = true;
uniform float matcapPowr = 1.0;
uniform float RimPowr = 1.0;
uniform sampler2D normalMap;
uniform sampler2D diffuseMap;
uniform sampler2D matcapMap;
uniform sampler2D AOMap;
uniform sampler2D rimMap;
uniform mat4 viewInverseMatrix;
vec3 fixNormalSample(vec3 v)
{
vec3 result = v - vec3(0.5,0.5,0.5);
result.y = flipY ? -result.y : result.y;
return result;
}
vec3 srgb_to_linear(vec3 c)
{
return pow(c,vec3(2.2,2.2,2.2));
}
vec3 linear_to_srgb(vec3 c)
{
return pow(c,vec3(0.4545,0.4545,0.4545));
}
void main()
{
vec3 cameraPosWS = viewInverseMatrix[3].xyz;
vec3 pointToCameraDirWS = normalize(cameraPosWS - iFS_PointWS);
vec3 pointToLight0DirWS = normalize(Lamp0Pos - iFS_PointWS);
vec3 pointToLight1DirWS = normalize(Lamp1Pos - iFS_PointWS);
vec3 normalWS = iFS_Normal;
vec3 tangentWS = iFS_Tangent;
vec3 binormalWS = iFS_Binormal;
// ------------------------------------------
// Update UV
vec2 uv = iFS_UV * tiling;
// ------------------------------------------
// Add Normal from normalMap
vec3 normalTS = texture2D(normalMap,uv).xyz;
vec3 cumulatedNormalWS = normalWS;
vec3 T = normalize(TtoV0.xyz);
vec3 B = normalize(TtoV1.xyz);
vec3 N = normalize(TtoV2.xyz);
if (length(normalTS)>0.0001)
{
normalTS = fixNormalSample(normalTS);
normalTS *= heightMapScale;
//vec3 normalMapWS = normalTS.x * tangentWS + normalTS.y * binormalWS;
vec3 normalMapWS = normalize(N + normalTS.y * T + normalTS.x * B);
cumulatedNormalWS = cumulatedNormalWS + normalMapWS;
cumulatedNormalWS = normalize(cumulatedNormalWS);
}
// Diffuse
vec3 diffuseColor = srgb_to_linear(texture2D(diffuseMap,uv).rgb);
// Matcap
vec2 abmUV;
abmUV.x = (T.x * normalTS.x + B.x * normalTS.y + N.x * normalTS.z) * 0.5 + 0.5;
abmUV.y = (T.y * normalTS.x + B.y * normalTS.y + N.y * normalTS.z) * -0.5 + 0.5;
//ambientOcclusionColor
vec3 AO_TEX = srgb_to_linear(texture2D(AOMap,uv).rgb);
vec3 Rim_TEX = srgb_to_linear(texture2D(rimMap,abmUV.xy).rgb);
Rim_TEX *=RimPowr;
vec3 ambientMapColor = srgb_to_linear(texture2D( matcapMap , abmUV.xy).rgb) * matcapPowr;
// ------------------------------------------
vec3 finalcolor = (diffuseColor.rgb * AO_TEX.rgb) * (ambientMapColor.rgb * ambientColor) + Rim_TEX.rgb;
// Final Color
gl_FragColor = vec4(linear_to_srgb(finalcolor), 1.0);
}
코드 중 아직 좀 미완성 부분도 있고 (AO_TEX 를 활용 한 감마콘트롤은 빠져있어요.) 대충 pseudo code 는 아래와 같긴 합니다.
한번 넣어 보세요.

그리고 전체 코드 중에… 앞으로 버전 2.0 에서 작업 할 예정인 다이나믹 라이트와의 혼용. 라이트를 나중에 또 추가 할 것이기 때문에 기존의 Lamp0 과 Lamp1 에 대한 변수등은 남겨 둔 상태 입니다.
위 코드를 보시면 RimTex 에 대한 부분이 있습니다. vec3 finalcolor = (diffuseColor.rgb * AO_TEX.rgb) * (ambientMapColor.rgb * ambientColor) + Rim_TEX.rgb; 로 만들었는데요. 가장 마지막에 Rim_TEX.rgb 를 더해 주도록 합니다.

Rim_TEX 는 따로 텍스처를 만들지 않았고 이 전 강좌에서 배웠던 필터를 사용 했고 Transform2D 를 중간에 추가 하여 림라이트의 주방향을 제어 할 수 있도록 하였습니다. 이것은 차 후 유니티 등에서 활용 될 수 있는 연결 변수 일 수 있습니다.

glslfx.xml 의 내용. Matcap 강도와 림텍스처의 강도에 대한 변수가 추가 되었습니다.

림라이트 텍스처의 패턴 컬러에 기본 색조를 만들고 HSL 값을 변경 하여 그림처럼 다양한 컬러가 들어 간 림텍스처를 만들어 보는 것도 좋은 방법입니다.

이런 식이 될 수 있겠죠.

1차로 나온 결과물 입니다. 여러 값을 수정 해 가면서 적당한 쉐이딩을 찾아 가면 될 듯 합니다.

전체 노드 맵.

모바일용 으로 제작 해 본 Matcap 확장 커스텀 쉐이더 적용 이미지.
서브스턴스 디자이너의 두개의 라이팅은 적용이 되지 않습니다.
사전 정의 된 2D 암비언트 텍스처로 베이스 라이팅 느낌을 주었고 림텍스처를 활용 해서
여러개의 간접광이 비추는 것 처럼 느낌을 내 봤습니다. 최대한 가벼운 상태로 적당한 느낌을 낼 수 있는 모바일 쉐이더 기반이라고 생각 하고 만들어 봤는데요… 어떻신가요?
해당 쉐이더는 유니티 엔진에서도 똑같이 적용 되며 대부분의 GLSL 기반에서 차이 없이 적용이 가능 합니다.
한달여 동안 차곡 차곡 서브스턴스 디자이너를 알아 봤습니다.
인터페이스 부터 시작 해서 필터와 활용… 합성 활용. 그리고 커스텀 쉐이더를 제작해서 서브스턴스에 적용 하고 실제 작업 해 보는 실전까지 살펴 봤습니다.
저는 모바일 게임개발을 하고 있기 때문에 물리기반 쉐이더나 그런 것에 관심이 많지만 실제 작업에서는 어떻게 하면 가볍게.. 그럴사 하게 돌아 가게 해 볼까 라는 것에 더 생각이 맞춰져 있습니다.
여기에 서브스턴스 디자이너의 강력한 기능을 접목 해 보는 것이야 말로 중요 하다고 생각 합니다.