TECHARTNOMAD | TECHARTFLOWIO.COM

MAYA

MAYA VRAY GLSL PROGRAMMING PART-2

jplee 2023. 5. 15. 02:15

저번 편에 이어서 이번 파트 2에서는 실제로 GLSL 과 Vray GLSL 의 빌트인 펑션들을 사용하여 셰이더를 만들어 보는 내용을 써 보려고 합니다.

 

MAYA VRAY GLSL PROGRAMMING PART-1

Custom material 을 제작하기 위해 Vray GLSL 을 사용하기 시작 하면서 알게 된 점들을 시리즈로 작성해 보기로 했습니다. 이 주제는 총 3개의 Parts 로 나뉘어 져 있습니다. Abstract ​ 마야(Maya)는 3D 그래

techartnomad.tistory.com

 

저번 편에서는 Vray GLSL API 내용들을 살펴 봤죠? 아마 위 링크페이지는 따로 열어 놓고 함께 따라 해 보는 것이 좋을 것 같네요.

 

 


목표

Lambert 라이트모델을 구성 합니다.

간단한 Fresnel reflectance 를 구현 합니다.

  • N : 법선벡터
  • L : 조명벡터
  • FB : Fresnel Bias
  • FS :Fresnel Scale
  • V : 카메라 벡터
  • F : Fresnel Power
  • $$ abs(dot(N , L)) +(FB + FS · pow(1+dot(N , V) , F)) $$
프레스넬(미국식 발음은 프레넬 이고 영국식 발음은 프레스넬 입니다.) 이펙트가 무엇인지 궁금하신 분은 아래 의 글을 읽어보시길 바랍니다.

 

 

Fresnel Effect에 대하여.

안녕하세요 흑룡해를 맞아 삼가 인사올립니다. T모사의 뭐든 궁금해 하는 배경아티스트/TA Silverchime 입니다. 오늘은 프레넬 효과Fresnel Effect에 관해 이야기해보려 합니다. 누구나 한번쯤 들어 보

gamedevforever.com

대략적으로 위와 같은 식을 참조하여 구현 하면 되겠네요. 이 정도만 구현 해 봐도 충분히 Vray GLSL API 에 대한 이해도 충분히 될것이고 쉽고 간단히 따라하면서 학습해 볼 수 있을겁니다. 그 이후에는 조금 씩 확장 해 나갈 수 있는거죠.

그럼 Lambert 식도 살펴 볼까요?

 

램버트 코사인 법칙(Lambert's cosine Law)

빛은 물체표면에 입사한 후, 물질 내에서 여러번 복잡하게 산란 후 표면을 빠져 나온다 여기서 [복잡하게]...

blog.naver.com

위 블로그 포스트를 살펴보는 것도 좋겠습니다.

확산반사인 Diffusion Reflectance 에 대한 에너지보존에 대해 관심이 있다면 이것을 살펴보도록 하자.

A Basic Introduction to BRDF-Based Lighting (princeton.edu)

 


기본 제공 형식

파트 1에서 이미 vr_LightIterator 에 대해서 살펴봤을겁니다.

vr_LightIterator 구조는 장면의 특정 광원에 대한 중요한 음영 데이터를 보유합니다. 이 정보를 얻으려면 vr_LightIterator 인스턴스를 마지막 인수로 사용하여 vr_evalLight 호출합니다. 모든 멤버는 선택한 조명에 필요한 데이터로 설정됩니다.

이렇게 설명 하고 있습니다.

구조체로 되어 있는데요.

위에서 람배르트 식에 대해서 살펴보고 오셨다면 이제 우리는 무엇을 해야 할 것인지 잘 알것입니다.

struct vr_LightIterator 
{
    vec3 direction; // 카메라 좌표에서 광원을 가리키는 단위 벡터입니다.
    float dot_nl; // 표면 법선(범프일 수 있음)과 빛 방향의 도트 곱입니다.
    vec3 contribution; // 대기 효과와 거리 감쇠를 포함한 이 라이트의 직접 광원 기여도입니다.
};

구조체는 이렇게 구성되어있죠.

아무튼 위의 구조체를 사용하여 light 를 선언합니다.

 

Lambert 구현이 완성 된 코드

//V-Ray Fragment Program 1.0

#version 110
__color uniform vec4 diffuseColor = vec4(0.6, 0.2, 1.0, 1.0);

void main()
{
    vec3 normal = (gl_FrontFacing) ? vr_Normal : -vr_Normal;
    vec3 viewDir = vr_Direction;

    vec4 Lambert = vec4(0.0, 0.0, 0.0, 1.0);
    vec3 raw_diffsue = diffuseColor.rgb;
    vec3 gi_contribution = vr_irradiance(normal, 1.0);

    vr_LightIterator light;

    for(int i = 0; i < vr_NumLights; ++i)
    {
        vr_evalLight(i, vr_Position, normal, light);
        float ndotl = clamp(dot(light.direction , normal),0.0,1.0);  
        //vec3 halfDir = normalize(viewDir + light.direction );
        //float ndoth = dot(normal, halfDir);

        if(ndotl > 0.0)
        {
            Lambert += vec4(vec3(ndotl) * light.contribution,0.0);
        }
    }
    Lambert.rgb *= raw_diffsue;
    Lambert.rgb += vec3(raw_diffsue * gi_contribution);
 
  
  // Output final color
  gl_FragColor.rgb = Lambert.rgb;
  gl_FragColor.a = 1.0;

}

먼저 람베르트가 구현 된 코드를 보고 중요한 부분들에 대해서 언급 하는 식으로 보는것이 좋겠습니다.

 

 

#version 110
__color uniform vec4 diffuseColor = vec4(0.6, 0.2, 1.0, 1.0);

형식 한정자를 사용하여 표면색상 프로퍼티를 선언합니다.

 

 

 

vec3 normal = (gl_FrontFacing) ? vr_Normal : -vr_Normal;

이제 실제로 계산을 수행 할 void main() 에서 작업을 해야 합니다.

Surface 의 표면의 노멀방향을 정의 합니다. 법선(노말)의 방향이 앞으로 향하고 있는지 뒤를 향하고 있는지 정의 할 수 있습니다.

 

 

vec3 viewDir = vr_Direction;

기본 제공 가변 변수인 varying vec3 vr_Direction 를 추가 합니다. 쉽게 알아보기 위해서 vec3 viewDir 변수를 하나 생성해서 거기에 넣어 줬어요.

 

 

vec4 Lambert = vec4(0.0, 0.0, 0.0, 1.0);
vec3 raw_diffsue = diffuseColor.rgb;

vec4 타입의 Lambert 형식으로 변수를 만들고 초기화 해 줍니다.

vec3 타입의 raw_diffuse 형식으로 변수를 만들고 형식한정자를 사용해서 선언했던 입력 프로퍼티를 연결 해 줍니다.

 

 

 

vec3 gi_contribution = vr_irradiance(normal, 1.0);

vec3 타입의 gi_contribution 변수를 만들고 빌트인 함수인 vr_irradiance() 를 넣어 줍니다.

원형은 vec3 vr_irradiance(vec3 normal, float importance) 로 되어 있기 때문에 먼저 정의 해 준 서피스의 normal 을 인자로 넣어 주고 중요도는 1.0 을 인자로 넣어 줍니다. importance 는 전역 조명 계산의 정밀도를 제어하는데 사용 됩니다. 범위는 0 또는 1 이어야 합니다. 빌트인 함수이기 때문에 구조의 동작원리를 깊게 설명하기에는 한계가 있군요.

vr_irradiance 을을 정의 하지 않으면 Vray 렌더링 결과물에서 이후 확인 할 때 표면의 GI 계산에 오류를 발생시키는데 표면 색상과 라이팅 계산 결과에 이상한 현상을 유발 시킵니다. 꼭 잊지 말고 정의 해 준 후 최종 아웃풋 직전에 계산을 해 줘야 할 것입니다.

 

 

 

vr_LightIterator light;

vr_LightIterator 는 기본 제공 형식의 데이터 구조체 입니다. 이것을 사용해서 light 데이터를 정의 할 수 있습니다.

매우 중요한 부분입니다. 이 구조는 장면의 특정 광원에 대한 중요한 음영 데이터를 보유합니다.

또한 이 정보를 얻기 위해서는 vr_evalLight 함수를 호출합니다. 모든 멤버변수는 선택한 조명에 필요한 데이터로 설정됩니다.

 

 

 

void vr_evalLight(int lightIndex, vec3 position, vec3 normal, out vr_LightIterator light);

빌트인 함수인 vr_evalLight 의 원형입니다.

for(int i = 0; i < vr_NumLights; ++i)
    {
        vr_evalLight(i, vr_Position, normal, light);
        float ndotl = clamp(dot(light.direction , normal),0.0,1.0);  
        if(ndotl > 0.0)
        {
            Lambert += vec4(vec3(ndotl) * light.contribution,0.0);
        }
    }

이 부분에서 갑자기 for문이 등장해야 하는지 이해하기 힘들 수도 있습니다.

장면의 모든 라이트 개수에 대해서 라이트모델을 계산 해 줘야 하기 때문이겠죠!

위에서 언급했던 vr_evalLight 함수를 호출 해 줘야 합니다. 이 전 파트1에서 이유가 나오죠.

 

인덱스 광원의 직접적인 조명 기여도를 평가합니다. 이 함수를 사용하여 셰이더의 직접 조명 조명을 평가할 수 있습니다.

이러한 역할을 하는 빌트임 함수인거죠.

이 함수에 for 문으로 획득한 라이트의 인덱스를 인자로 넣어 주고 포지션과 노말에 대한 데이터를 인자로 넣어 주면 이제 out 데이터 light 에 대한 평가를 끝내서 돌려 주는 것이죠.

이 부분이 중요한 부분인데요... 조명모델을 추가 할 경우 입력 프로퍼티로 받아온 서피스 표면의 입력데이터등을 제외 하고는 이 반복문 안에서 모두 계산을 해야 하는것이 되는 것이죠.

저는 람베르트 식을 위해서 float ndotl 을 추가 하고 여기에 clamp(dot(light.direction , normal),0.0,1.0) 을 넣어줬죠. ( 하지만 실제로 vr_LightIterator 원형을 보면 알 수 있듯이 light.dot_nl 을 사용해서 바로 람베르트식을 근사 할 수 있습니다. )

라이트가 가르키는 방향과 서피스의 법선방향을 내적하면 간단하게 람베르트 확산 반사 조명모델을 만들 수 있습니다.

ndotl 이 0.0 보다 클 경우에만 계산을 해 주면 되기 때문에 이렇게 분기문을 사용했습니다.

 

0.0 보다 큰 경우에 Lambert 에 vec4(vec3(ndotl) * light.contribution,0.0) 을 더해주웠죠. light.contribution 은 대기효과와 거리감감쇠를 포함한 라이트의 직접 관여에 대 기여도를 뜻합니다.
Lambert.rgb *= raw_diffsue;
Lambert.rgb += vec3(raw_diffsue * gi_contribution);

이제 반복문 밖에서 최종 마무리를 해 줍시다.

위에서 선언 했었던 raw_diffuse 는 서피스의 표면 색이었습니다.

이것은 굳이 반복문 안에서 계산 할 필요가 없는 것이죠. 람베르트 확산 조명 모델을 모든 장면의 라이트 개수 만큼 계산 한 이후에 표면 색상을 곱해주면 되는 것이죠.

그 다음 이 값에 gi_contribution 을 곱한 값을 다시 더해 주면 됩니다. 위에서 미리 이것에 대해서 언급 했던 것을 기억 할 것입니다.

 

 

vr_irradiance() 의 반환 값은 GI 평가의 결과에 따른 rgb 색상이며 이렇게 계산을 해 주면 표면의 색상(또는 표면에 적용 된 텍스처 등등)의 결과를 정확하게 출력 할 수 있습니다.

여기까지 기본적인 람베르트 확산 조명 모델이었습니다.


Fresnel effect 구현

이런 효과는 많이 보셨을겁니다.

개인적으로 좀 더 물리적인 개념에서 접근한 프레넬 방정식 근사를 참고 하시려면 아래 링크 카드에서 찾아볼 수 있습니다.

 

LearnOpenGL - theory

Theory pbr/theory PBR, or more commonly known as physically based rendering, is a collection of render techniques that are more or less based on the same underlying theory that more closely matches that of the physical world. As physically based rendering

learnopengl.com

 

하지만 우리는 학습을 위해 매우 단순하고 고전적인 계산식을 사용해 보죠.

$$ Fresnel = (FB + FS · clamp(pow(1+dot(N , V) ,0.0, 1.0) , F)) $$

 

UE4 에서 Fresnel 머티리얼 표현식 노드는 표면 노멀과 카메라까지의 방향의 내적을 기반으로 감쇠 계산을 합니다. 표면 노멀이 카메라를 바로 향하면 출력값은 0, 즉 발생하는 프레넬 이펙트가 없습니다. 표면 노멀이 카메라에 수직인 경우 출력값은 1, 즉 최대치의 프레넬 이펙트가 발생합니다. 그런 다음 그 결과를 [0,1] 범위로 제한시켜 가운데 음수 컬러가 생기지 않도록 합니다. 다음 그림은 이 개념을 나타냅니다. (발췌. 언리얼엔진 문서)

 

 

머티리얼에 프레넬 사용하기

Fresnel 머티리얼 노드 사용법 안내입니다.

docs.unrealengine.com

 

대략 이러한 구성을 띄고 있습니다. 가장 먼저 입력 프로퍼티를 추가 해야 되겠습니다.

대부분의 입력 프로퍼티는 단일값의 형식이라 vec 이런식이 아닌 float 입니다.

이러한 입력 프로퍼티는 마야에서 입력되어 들어 오기 때문에 uniform 으로 정의 됩니다.

이런 식이죠.

uniform float fresnelBias = 1.0;
uniform float fresnelScale = 1.0;
unifor float fresnelPower = 1.0;

main 함수 이 전에 float 형의 Fresnel 이라고 변수를 하나 선언 해 줍니다.

vec4 Lambert = vec4(0.0, 0.0, 0.0, 1.0);
vec3 raw_diffsue = diffuseColor.rgb;

float Fresnel = 0.0; //Frensel variable here and initalized to zero

그리고 1.0 으로 초기화 해 주면 됩니다.

Fresnel 변수에 이제 계산식을 넣어 줍시다.

이미 카메라 벡터는 알고 있습니다. 

vec3 viewDir = vr_Direction; // 기본 제공 가변 변수 API 에서 vr_Direction 을 viewDir 변수에 담음.

기본 제공 가변 변수의 vr_Direction 을 이미 viewDir 변수에 담아놨었다는 것을 기억하십시오.

vec3 normal = (gl_FrontFacing) ? vr_Normal : -vr_Normal;

또한 normal 변수에 카메라 공간 표면 교차점에 있는 서피스 표면 법선 단위 벡터를 담아 놨습니다.

이제 거의 다 됬네요.

미리 선언 해 둔 uniform 변수들과 조립해 봅시다.

 

Fresnel = fresnelBias + fresnelScale * clamp((pow(1.0 + dot(normal , viewDir) , fresnelPower)),0.0 , 1.0);

수식에 따르자면 이렇게 구현 될 수 있습니다.

핵심 부분은 위 참조 문서에서 언급 했듯이 카메라 시야각으로 부터 서피스의 표면과의 내적을 구해서 그 현상을 간략히 시뮬레이션 하기 때문이죠.

구체 중앙의 0 인 부분은 프레넬 이펙트가 없는 것이 보입니다. 왜냐면 카메라가 바로 표면 노멀쪽을 향하고 있기 때문입니다. 표면 노멀과 카메라가 수직이 되어갈 수록, 즉 1 에 가까울 수록 프레넬 이펙트가 더욱 잘 보이게 되는데, 바로 그러한 작동방식 유형이 필요한 것입니다. ( 발췌. 언리얼 문서 )

내적을 구한 후 pow 를 사용하여 거듭제곱 하면 효과의 강도를 제어 할 수 있습니다. 또한 내적의 값이 [0 ~ 1]  범위 안에서 출력 되도록 clamp() 로 내적 결과를 감싸줘야 합니다.

 

clamp - OpenGL 4 Reference Pages

 

registry.khronos.org

 

간단한 프레넬 효과가 완성 된 Vray GLSL 코드

#version 110
__color uniform vec4 diffuseColor = vec4(0.6, 0.2, 1.0, 1.0);
uniform float fresnelBias = 0.5;
uniform float fresnelScale = 1.0;
uniform float fresnelPower = 5.0;

void main()
{
    vec3 normal = (gl_FrontFacing) ? vr_Normal : -vr_Normal;
    vec3 viewDir = vr_Direction;
    
    vec4 Lambert = vec4(0.0, 0.0, 0.0, 1.0);
    vec3 raw_diffsue = diffuseColor.rgb;
    vec3 gi_contribution = vr_irradiance(normal, 1.0);
    float Fresnel = 0.0;
    Fresnel = fresnelBias + fresnelScale * clamp((pow(1.0 + dot(normal , viewDir) , fresnelPower)),0.0 , 1.0);

    vr_LightIterator light;

    for(int i = 0; i < vr_NumLights; ++i)
    {
        vr_evalLight(i, vr_Position, normal, light);
        float ndotl = clamp(dot(light.direction , normal),0.0,1.0);  
        
        if(ndotl > 0.0)
        {
            Lambert += vec4(vec3(ndotl) * light.contribution,0.0);
        }
    }

    Lambert.rgb +=Fresnel;
    Lambert.rgb *= raw_diffsue;
    Lambert.rgb += vec3(raw_diffsue * gi_contribution);
    
  
  // Output final color
  gl_FragColor.rgb = Lambert.rgb;
  gl_FragColor.a = 1.0;

}

IPR VIEWPORT
VRAY RENDERING BUFFER VIEW

 

VRAY GLSL 의 경우 아직 IPR 렌더링 상에서의 데이터 갱신에 문제가 많습니다.

실제로 프로젝트에 적용하기 위해서는 정상적인 VRAY RENDERING 으로 아웃풋을 출력하면 문제가 없습니다.

'MAYA' 카테고리의 다른 글

[IDE]PyCharm + MayaCharm + API AutoComplete  (0) 2023.11.09
MAYA VRAY GLSL PROGRAMMING PART-3  (0) 2023.05.16
MAYA VRAY GLSL PROGRAMMING PART-1  (0) 2023.05.14