본문 바로가기

GameDevelopmentDiary/ETC

유니티용 복셀 셰이더 with ChatGPT

1. Voxel art 스타일 셰이더 생성

2. 라이팅 설정


 

1.  Voxel art 스타일 셰이더 생성
  이유:

  만약 1인 개발을 한다고 가정해봤다. Voxel art 스타일이 너무 예쁜데 상상임에도 불구하고 접근을 주저하게 되었다. 리소스 제작에 들어가는 부담이 크기 때문이었다. 일일히 모델링 하고 애니메이션 만들자니 시간이 너무 걸리고, 해놓은 리소스를 구하자니 1voxel 단위 맞추기도 쉽지 않겠다는 생각이 들었다.

  그렇다면 Pixel2D 느낌의 셰이더처럼 voxel 셰이더도 만들어보면 괜찮겠다는 생각으로 코드를 두드려보았다.

 

  Chat GPT 사용한 제작기: 

더보기

  유니티 엔진에서 사용할 shader 제작이나 편집을 마지막으로 해본 때가 어디보자... 한 7,8년 쯤 되었는데 기본 ,shader 파일 첫줄부터 낯설다.

Shader "Custom/NewSurfaceShader"
{
    ...
}

 

  요즘 내 1일 평균 대화량 top 순위인 ChatGPT에게 도움을 청했다. 첫 질문은 안 보이는데, 얼추 이런 질문이었다.

 

  "내가 유니티 엔진에 사용할 커스텀 셰이더 파일을 만들거야. 이름은 VoxelShader야. 일정 vertex 거리마다 Voxel이 만들어져야해. 텍스쳐와 컬러도 매개변수로 사용할거야."

 

  처음에는 코드의 틀은 잡아 주었지만 voxel 개념을 제대로 반영해주진 않았다. 유니티에서는 geomatry shader를 사용할 수 없다는 말도 나왔다.

...
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag       
            #include "UnityCG.cginc"
            ...
    	}
...

 

  이후 몇 번을 더 티키타카하다가 일단 구글링에서 찾은 복셀 셰이더 코드 일부를 <https://github.com/mika-archived/Unity-VoxelShader>를 샘플로 보여줬다. 그리고 ' directx10부터 Geometry Shader 추가한걸로 알고 있다'는 사실적인 말을 추가하니까 그제야 타겟을 설정하라고 해줬다.(우리말로 대답 잘 하다가 굳이 영어로!)

 

  1. Shader Model Compatibility: Ensure your shader model supports Geometry Shaders. This typically means using #pragma target 5.0 or higher in Unity.

 이후에는 자세한 대답은 좀 아쉬웠다. Cube 그리기 위한 꼭지점의 위치와 법선에 오류가 많았다.

[maxvertexcount(36)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
{
    float3 positions[8] = {
        float3(-0.5, -0.5, 0.5) * _CubeSize, // 앞면의 좌하단
        float3(0.5, -0.5, 0.5) * _CubeSize,  // 앞면의 우하단
        float3(-0.5, 0.5, 0.5) * _CubeSize,  // 앞면의 좌상단
        float3(0.5, 0.5, 0.5) * _CubeSize,   // 앞면의 우상단
        float3(-0.5, -0.5, -0.5) * _CubeSize,// 뒷면의 좌하단
        float3(0.5, -0.5, -0.5) * _CubeSize, // 뒷면의 우하단
        float3(-0.5, 0.5, -0.5) * _CubeSize, // 뒷면의 좌상단
        float3(0.5, 0.5, -0.5) * _CubeSize   // 뒷면의 우상단
    };

    int indices[36] = {
        0, 1, 2,  2, 3, 0,  // front face
        4, 5, 6,  6, 7, 4,  // back face
        0, 2, 4,  4, 6, 2,  // left face
        1, 3, 5,  5, 7, 3,  // right face
        0, 1, 4,  4, 5, 1,  // bottom face
        2, 3, 6,  6, 7, 3   // top face
    };

    ...
    
    triStream.RestartStrip();
}

 

 육각면체의 한 면을 두 개의 삼각형으로 만드는데, 두 삼각형이 겹치거나 법선 방향이 반대인 경우가 발생한다.

 그래서 이 부분은 개인적으로 시계방향, 반시계방향 고려하여 법선 설정해서 수정했다.

 이렇게 만들다 보니 결과적으로 각 vertex에 큐브를 생성할 수 있었다. 

<원본 _ 제페토용 모자 모델링>
< VoxelShader 1차 샘플 - 법선 수정 전>
<VoxelShader 1차 샘플 - 법선 수정 후>

 

  하지만 조명도 받지 못하고 있고, 큐브가 겹쳐지는 모습이 심히 아름답지 못하다.

  우선 큐브 간격부터 조절했다.

  크기를 결정하는 변수 외에도 큐브간 간격을 설정할 변수도 추가해줬다. geomatry로 넘기기 전에 조정했다.

v2g vert(appdata v)
{
    v2g o;
    o.pos = float4(floor(v.vertex.xyz / _PixelSize) * _PixelSize, 1.0);
    o.normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);

    // 직접 UV 변환 수행
    float2 scaledUV = v.uv * _MainTex_ST.xy;
    float2 offsetUV = scaledUV + _MainTex_ST.zw;
    o.uv = offsetUV;

    // 색상 계산
    o.color = tex2Dlod(_MainTex, float4(o.uv, 0, 0)) * _Color;

    return o;
}

 

<VoxelShader 2차 샘플 A>

훨씬 나아졌다. 다만 기본 모델의 정점 간격이 비교적 일정해야 한다는 제한도 발견했다. 

 

<VoxelShader 2차 샘플 B>

  정점 간 간격이 좁은 꽃잎은 모양이 괜찮은데, 풀잎은 그냥 점선으로 보일 수 있다는 단점이 생긴다.

  ChatGPT야, 그러면 이제 조명도 추가해줘!

<ChatGPT의 답변 중 발췌>
< VoxelShader 3차 샘플>

  텍스쳐는 잘 사용하다가.. 하.. 아니다. 참고만 할게..

fixed4 col = tex2D(_MainTex, IN.uv);

float3 norm = normalize(IN.normal);
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float diff = max(dot(norm, lightDir), 0.0);
col.rgb *= _Color * diff;
return col;

 

  끝났나?

< VoxelShader 4차 샘플 >

 

  유니티의 _WorldSpaceLightPos0 조명은 메인 조명의 정보만 가져다준다.

  그런데 만약 조명이 하나가 아니라면?

  4차샘플 이미지 속에 보이는 녹색 캐릭터는 추가 포인트 조명에 물들었는데, voxel덩어리는 물들지 않은 이유이다. 

  그래서 frag 셰이더 코드에  unity_4Light 관련 빌트인 변수들도 추가해줘야한다.

  이 때 중요한점은 ForwardBase  ForwardAdd 패스 타입만 가능하다.

  만약 다른 타입으로 설정해도 그냥 무시되지 오류 메시지도 안 띄워줘서 난항을 겪었다. 

 

...
SubShader
    {
        Tags { "LightMode" = "ForwardBase"}
        LOD 100
        
        Pass
        {
        	...
            fixed4 frag(g2f IN) : SV_Target
            {
            	...
                // main light 
                float3 norm = normalize(IN.normal);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);                    
                float diff = max(dot(norm, lightDir), 0.0);
                col.rgb *= _Color * diff;

                // sub lights
                for (int i = 0; i < 4; i++) {
                    float dist = (1 / min(distance(float3(unity_4LightPosX0[i], unity_4LightPosY0[i], unity_4LightPosZ0[i]), 0.001), IN.pos));
                    float3 lightDir = normalize(float3(unity_4LightPosX0[i], unity_4LightPosY0[i], unity_4LightPosZ0[i]));
                    float diff = max(dot(norm, lightDir), 0.0);
                    col.rgb += _Color * dist * unity_4LightAtten0[i] * diff * unity_LightColor[i];
                }
                ...
            }
            ENDCG
        }
    }
    ...

 

이렇게 모두 수정을하고 나면 완성! 

 

복셀 셰이더 샘플

 

< VoxelShader 보조 조명 반응 >

 

비교 이미지

< VoxelShader 프로퍼티 설정 비교 이미지 >

 

완성 코드

더보기
// hlsl
// 코드를 참조하거나 사용해보고 생긴 문제점이나 발견하신 개선점을 댓글로 남겨주세요. 감사합니다.

Shader "Custom/VoxelShader"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
        _Color("Main Color", Color) = (1,1,1,1)
        _PixelSize("Pixel Size", Float) = 0.1
        _CubeSize("Cube Size", Float) = 1.0
        _IsUnlit("bIsUnlit", Float) = 1.0
    }
    SubShader
    {
        Tags { "LightMode" = "ForwardBase"}
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
            #pragma target 5.0            
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2g
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
                half2 uv : TEXCOORD0;
                fixed4  color : COLOR0;
            };

            struct g2f
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
                half2 uv : TEXCOORD0;
                fixed4  color : COLOR0;
                UNITY_FOG_COORDS(1)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            uniform float _PixelSize;
            uniform float4 _Color;
            uniform float _CubeSize;
            uniform float _IsUnlit;

            v2g vert(appdata v)
            {
                v2g o;

                o.pos = float4(floor(v.vertex.xyz / _PixelSize) * _PixelSize, 1.0);                

                o.normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);

                float2 scaledUV = v.uv * _MainTex_ST.xy;
                float2 offsetUV = scaledUV + _MainTex_ST.zw;
                o.uv = offsetUV;                

                o.color = tex2Dlod(_MainTex, float4(o.uv, 0, 0)) * _Color;

                return o;
            }

            [maxvertexcount(36)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                float3 positions[8] = {
                    float3(-0.5, -0.5, 0.5) * _CubeSize, // front right down
                    float3(0.5, -0.5, 0.5) * _CubeSize,  // front left down
                    float3(-0.5, 0.5, 0.5) * _CubeSize,  // front right top
                    float3(0.5, 0.5, 0.5) * _CubeSize,   // front left top
                    float3(-0.5, -0.5, -0.5) * _CubeSize,// back right down
                    float3(0.5, -0.5, -0.5) * _CubeSize, // back left down
                    float3(-0.5, 0.5, -0.5) * _CubeSize, // back right top
                    float3(0.5, 0.5, -0.5) * _CubeSize   // back left top
                };

                int indices[36] = {
                    0, 1, 3,  3, 2, 0,  // front face
                    5, 4, 6,  6, 7, 5,  // back face
                    0, 2, 4,  2, 6, 4,  // left face
                    7, 3, 1,  5, 7, 1,  // right face
                    4, 1, 0,  4, 5, 1,  // bottom face
                    2, 3, 6,  3, 7, 6   // top face
                };

                float3 dirIndexes[6] = {
                    float3(0, 0, 1), // front
                    float3(0, 0, -1), // back
                    float3(-1, 0, 0), // left
                    float3(1, 0, 0), // right
                    float3(0, -1, 0), // bottom
                    float3(0, 1, 0) // top
                };

                for (int i = 0; i < 36; i++)
                {

                    if (i % 3 == 0) {
                        g2f o1, o2, o3;
                        o1.pos = UnityObjectToClipPos(float4(IN[0].pos.xyz + positions[indices[i]], 1.0));
                        o2.pos = UnityObjectToClipPos(float4(IN[0].pos.xyz + positions[indices[i+1]], 1.0));
                        o3.pos = UnityObjectToClipPos(float4(IN[0].pos.xyz + positions[indices[i+2]], 1.0));

                        o1.normal = dirIndexes[floor(i / 6)];
                        o2.normal = dirIndexes[floor(i / 6)];
                        o3.normal = dirIndexes[floor(i / 6)];

                        o1.uv = IN[0].uv;
                        o2.uv = IN[0].uv;
                        o3.uv = IN[0].uv;

                        o1.color = IN[0].color;
                        o2.color = IN[0].color;
                        o3.color = IN[0].color;                        

                        triStream.Append(o1);
                        triStream.Append(o2);
                        triStream.Append(o3);

                        triStream.RestartStrip();
                    }
                }
            }

            fixed4 frag(g2f IN) : SV_Target
            {

                fixed4 col = tex2D(_MainTex, IN.uv);                

                if (_IsUnlit > 0) {
                    col.rgb *= _Color;
                    return col;
                }
                else {
                    // main light 
                    float3 norm = normalize(IN.normal);
                    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);                    
                    float diff = max(dot(norm, lightDir), 0.0);
                    col.rgb *= _Color * diff;

                    // sub lights
                    for (int i = 0; i < 4; i++) {
                        float dist = (1 / min(distance(float3(unity_4LightPosX0[i], unity_4LightPosY0[i], unity_4LightPosZ0[i]), 0.001), IN.pos));
                        float3 lightDir = normalize(float3(unity_4LightPosX0[i], unity_4LightPosY0[i], unity_4LightPosZ0[i]));
                        float diff = max(dot(norm, lightDir), 0.0);
                        col.rgb += _Color * dist * unity_4LightAtten0[i] * diff * unity_LightColor[i];
                    }

                    return col;
                }                
            }
            ENDCG
        }
    }
}

 

ChatGPT 사용 의의

  직접적인 정보는 구글링이 효율적이었다.

  그래도 작업 속도가 확실히 빨라졌다. 직접적인 방법을 알려주진 않았지만, ChatGPT에게 물어보기 위해 문제를 정리하는 과정에서 다시 한 번 정리가 되거나, 새로운 접근 방법을 찾게 되었다.  

 

참고

  유니티 공식 문서(빌트인 셰이더 변수):  https://docs.unity3d.com/kr/2021.1/Manual/SL-UnityShaderVariables.html

  Mika-archived VoxelShader: https://github.com/mika-archived/Unity-VoxelShader  

  유니티 포럼(셰이더에 포인트라이트 사용하기): https://forum.unity.com/threads/point-light-in-v-f-shader.499717/

'GameDevelopmentDiary > ETC' 카테고리의 다른 글

Install Google MediaPipe  (0) 2022.06.18