[Unity] URP 셰이더 퐁(Phong), 블린 퐁(Blinn Phong), 프레넬(Fresnel), Vertex Shader 연산(Gouraud Shading)

2023. 6. 13. 15:22Unity

728x90
반응형

퐁(Phong)

Bui Tuong Phong이 1975년 발표한 렌더링 알고리즘이다.

 

 

퐁 스페큘러 계산의 원리를 설명하는 그림이다. 스페큘러 효과로 얼마나 밝아지는지, 즉 반사된 광원이 얼마나 카메라로 정확하게 들어오는지를 계산하기 위해 R 벡터와 V 벡터의 각도 차이를 계산한다. 차이가 작으면 반사된 광원이 카메라에 정확하게 보인다는 뜻이고 크면 광원이 카메라에 안 보인다는 뜻이다. dot(R, V)를 계산한 결과의 스칼라 값에 의해 얼마나 밝은지가 결정된다.

빛의 반사는 HLSL reflect(LI, N)에 의해 계산된다.

 

 

Shader "Custom/BRDF/Phong"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [HDR]_SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecPower("Specular Power", Float) = 10
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
                float3 normalOS     : NORMAL;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
                float3 normal       : TEXCOORD1;
                float3 lightDir     : TEXCOORD2;
                float3 viewDir      : TEXCOORD3;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _SpecColor;
                half _SpecPower;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.normal = TransformObjectToWorldNormal(IN.normalOS);
                OUT.lightDir = normalize(_MainLightPosition.xyz);
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                OUT.viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                // 이걸 안 하면 버텍스 사이 픽셀 노멀의 길이가 1이 아닌 것들이 발생함.
                IN.normal = normalize(IN.normal);

                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                float NdotL = saturate(dot(IN.normal, IN.lightDir));
                
                // Phong Specular
                float3 reflectDir = reflect(-IN.lightDir, IN.normal);
                half spec = saturate(dot(reflectDir, IN.viewDir));
                spec = pow(spec, _SpecPower);
                half3 specColor = spec * _SpecColor.rgb;

                half3 ambient = SampleSH(IN.normal);
                half3 lighting = NdotL * _MainLightColor.rgb + ambient;
                color.rgb *= lighting;
                color.rgb += specColor;

                return color;
            }
            ENDHLSL
        }
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
    }
}

 

 

        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [HDR]_SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecPower("Specular Power", Float) = 10

스페큘러를 위해 이전과는 다르게 추가로 필요한 프로퍼티이다. _SpecColor는 강하게 증폭할 수 있도록 [HDR] 속성을 부여했다. _SpecPower 값이 커지면 스페큘러가 작고 또렷한 느낌이 든다. 값이 작아지면 스페큘러가 넓고 흐리게 퍼진다. "Glossiness"라는 단어로 표기되는 경우가 많다.

 

 

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
                float3 normal       : TEXCOORD1;
                float3 lightDir     : TEXCOORD2;
                float3 viewDir      : TEXCOORD3;
            };

viewDir 변수는 Varyings 구조체의 멤버로 버텍스에서 카메라를 바라보는 벡터를 프래그먼트 셰이더로 전달하는 역할을 한다.

 

 

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _SpecColor;
                half _SpecPower;
            CBUFFER_END

SRP Batcher를 위해 CBUFFER에서 해당 프로퍼티를 관리하는 내용이다. _SpecPower 변수도 추가되었다.

 

 

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.normal = TransformObjectToWorldNormal(IN.normalOS);
                OUT.lightDir = normalize(_MainLightPosition.xyz);
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                OUT.viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
                return OUT;
            }

TransformObjectToWorld 함수로 버텍스의 월드 좌표를 계산해서 이후 viewDir 계산에 사용한다.

OUT.viewDir은 카메라를 바라보는 벡터를 계산한다. 카메라 위치에서 현재 버텍스 위치를 빼면 카메라를 향하는 벡터가 나오는데 이것을 정규화해서 크기를 1로 만든다. 정규화하는 이유는 버텍스 셰이더에서 계산 후 보간기를 거친 픽셀의 노멀 정보는 오차가 있을 수 있기 때문이다. 라이트나 카메라 방향도 마찬가지이다. 오차의 위험에도 버텍스 셰이더에서 계산 후 보간기를 거치는 이유는 버텍스 연산이 픽셀 연산에 비해 대체로 가볍기 때문이다. 최적화에 도움이 된다.

 

 

                // Phong Specular
                float3 reflectDir = reflect(-IN.lightDir, IN.normal);
                half spec = saturate(dot(reflectDir, IN.viewDir));
                spec = pow(spec, _SpecPower);
                half3 specColor = spec * _SpecColor.rgb;

퐁 반사는 빛의 반사 벡터를 먼저 구해야한다. reflect에 빛의 입사 벡터와 픽셀의 노멀 벡터를 인수로 사용했다. -IN.lightDir 음수를 사용한 이유는 dot 연산에 사용할 목적으로 빛을 바라보는 벡터였으므로 정 반대 방향을 향하게 하기 위함이다.

spec에는 1차적인 스페큘러 계산 결과가 기록된다. 반사 벡터와 시서 벡터의 방향 차이를 dot 함수를 사용해서 반사된 빛이 얼마나 카메라를 향하고 있는지를 계산한다.

그 아래 줄은 power 함수를 이용해 얼마나 스페큘러를 조절할지를 결정한다.

그다음 스페큘러 컬러를 곱하는데 _SpecColor는 HDR 값이 가능하므로 증폭도 이루어진다.

 

 

                half3 ambient = SampleSH(IN.normal);
                half3 lighting = NdotL * _MainLightColor.rgb + ambient;
                color.rgb *= lighting;
                color.rgb += specColor;

마지막 줄에 최종 컬러에 SpecColor 값을 더해주면서 퐁 반사가 완성된다.

 

 


 

 

블린 퐁(Blinn Phong)

퐁 반사 모델과 마찬가지로 동그란 스페큘러 광원 느낌을 만든다. 퐁의 reflect 함수 부분을 하프 벡터를 사용함으로써 조금 더 가볍게 연산한다. 퐁과 블린 퐁 모두 정확한 정반사는 아니기 때문에 유사 정반사라면 가벼운 쪽을 사용하는 게 좋을 수 있다.

H로 표기된 화살표가 하프 벡터를 의미한다. L과 V를 더하여 정규화 하면 H 값이 나오게 된다. 이후 픽셀 노멀 방향 N과 H를 dot로 각도 비교를 하여 얼마나 일치하는지 검사한다. 단순한 덧셈에 의해 H를 계산하므로 reflect를 사용하는 퐁 반사 모델보다 가볍다.

 

 

Shader "Custom/BRDF/BlinnPhong"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [HDR]_SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecPower("Specular Power", Float) = 10
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
                float3 normalOS     : NORMAL;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
                float3 normal       : TEXCOORD1;
                float3 lightDir     : TEXCOORD2;
                float3 viewDir      : TEXCOORD3;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _SpecColor;
                half _SpecPower;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.normal = TransformObjectToWorldNormal(IN.normalOS);
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                OUT.viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
                OUT.lightDir = normalize(_MainLightPosition.xyz);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                // 이걸 안 하면 버텍스 사이 픽셀 노멀의 길이가 1이 아닌 것들이 발생함.
                IN.normal = normalize(IN.normal);

                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                float NdotL = saturate(dot(IN.normal, IN.lightDir));
                
                // BlinnPhong Specular
                float3 H = normalize(IN.lightDir + IN.viewDir);
                half spec = saturate(dot(H, IN.normal));
                spec = pow(spec, _SpecPower);
                half3 specColor = spec * _SpecColor.rgb;

                half3 ambient = SampleSH(IN.normal);
                half3 lighting = NdotL * _MainLightColor.rgb + ambient;
                color.rgb *= lighting;
                color.rgb += specColor;

                return color;
            }
            ENDHLSL
        }
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
    }
}

 

 

                // BlinnPhong Specular
                float3 H = normalize(IN.lightDir + IN.viewDir);
                half spec = saturate(dot(H, IN.normal));
                spec = pow(spec, _SpecPower);
                half3 specColor = spec * _SpecColor.rgb;

퐁과 비교하여 두 줄이 바뀌었다. H는 앞서 그래프에서 보았듯이 광원과 뷰 벡터를 더하여 정규화하는 과정에서 계산할 수 있고 spec 변수는 H와 픽셀 노멀을 dot 계산하여 구할 수 있다. 주의할 점은 IN.lightDir 방향에 마이너스가 없다는 것이다.

 

 


 

 

프레넬(Fresnel)

 

[Unity] URP 셰이더를 위해 알아야 할 것들

셰이더란 무엇인가? -화면에 그려지는 픽셀의 색을 결정하는 프로그램. GPU 내부에서 실행되기 때문에 여러 픽셀에 대해서 동일한 셰이더가 동시에 실행된다. Graphics API 종류와 프로세스 -Direct3D,

lightbakery.tistory.com

앞선 글에서 프레넬에 대한 간략한 설명을 했던 적이 있다. 비스듬한 각도에서 바라볼수록 정반사가 강해지는 특성이다. 램퍼트 코사인 법칙과 유사하지만 광원 대신 카메라를 사용하는 점이 다르다.

 

 

Shader "Custom/BRDF/BlinnPhongFresnel"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [HDR]_SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecPower("Specular Power", Float) = 10
        [HDR]_FresnelColor("Fresnel", Color) = (0.2, 0.2, 0.2)
        _FresnelPower("Fresnel Power", Float) = 4
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
                float3 normalOS     : NORMAL;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
                float3 normal       : TEXCOORD1;
                float3 lightDir     : TEXCOORD2;
                float3 viewDir      : TEXCOORD3;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _SpecColor;
                half _SpecPower;
                half4 _FresnelColor;
                half _FresnelPower;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.normal = TransformObjectToWorldNormal(IN.normalOS);
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                OUT.viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
                OUT.lightDir = normalize(_MainLightPosition.xyz);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                // 이걸 안 하면 버텍스 사이 픽셀 노멀의 길이가 1이 아닌 것들이 발생함.
                IN.normal = normalize(IN.normal);

                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                float NdotL = saturate(dot(IN.normal, IN.lightDir));
                
                // BlinnPhong Specular
                float3 H = normalize(IN.lightDir + IN.viewDir);
                half spec = saturate(dot(H, IN.normal));
                spec = pow(spec, _SpecPower);
                half3 specColor = spec * _SpecColor.rgb;

                // Fresnel
                half fresnel = 1 - saturate(dot(IN.normal, IN.viewDir));
                fresnel = pow(fresnel, _FresnelPower);
                half3 fresnelColor = fresnel * _FresnelColor.rgb;

                half3 ambient = SampleSH(IN.normal);
                half3 lighting = NdotL * _MainLightColor.rgb + ambient;
                color.rgb *= lighting;
                color.rgb += specColor + fresnelColor;

                return color;
            }
            ENDHLSL
        }
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
    }
}

 

 

Shader "Custom/BRDF/BlinnPhongFresnel"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [HDR]_SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecPower("Specular Power", Float) = 10
        [HDR]_FresnelColor("Fresnel", Color) = (0.2, 0.2, 0.2)
        _FresnelPower("Fresnel Power", Float) = 4
    }

[HDR] 속성을 부여하여 _FresnelColor 변수를 선언하였고 _FresnelPower도 추가되었다.

 

 

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _SpecColor;
                half _SpecPower;
                half4 _FresnelColor;
                half _FresnelPower;
            CBUFFER_END

CBUFFER에서도 마찬가지로 새로운 변수가 추가되어 있는 것을 볼 수 있다.

 

 

                // Fresnel
                half fresnel = 1 - saturate(dot(IN.normal, IN.viewDir));
                fresnel = pow(fresnel, _FresnelPower);
                half3 fresnelColor = fresnel * _FresnelColor.rgb;

                half3 ambient = SampleSH(IN.normal);
                half3 lighting = NdotL * _MainLightColor.rgb + ambient;
                color.rgb *= lighting;
                color.rgb += specColor + fresnelColor;

노멀 벡터와 뷰 벡터를 dot 연산한 뒤 saturate하고 1에서 빼서 반전하여 fresnel 값을 구할 수 있다. 이후는 블린 퐁과 같은 방식으로 pow 함수를 적용하고 프레넬 컬러를 곱한다.

마지막 줄에서 최종 컬러에 스페큘러 컬러와 프레넬 컬러를 더한다.

 

 


 

 

Gouraud Shading

램버트 라이팅 연산을 버텍스 셰이더에서 계산하는 방식이다.

블린 퐁 스페큘러 연산을 버텍스 셰이더에서 수행한 결과이다. 구 위에 각 버텍스 위치에서 라이팅 연산을 하고 나머지 픽셀들은 라이팅 연산 없이 버텍스 라이팅 결과를 보간하기 때문에 저렇게 보이게 된다.

 

 

Shader "Custom/BRDF/BlinnPhongVertexShader"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [HDR]_SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecPower("Specular Power", Float) = 10
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
                float3 normalOS     : NORMAL;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
                half3 lighting      : COLOR0;
                half3 specColor     : COLOR1;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _SpecColor;
                half _SpecPower;
            CBUFFER_END

            // Lambert 라이팅 연산을 대부분 버택스 셰이더에서 수행
            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                float3 normalWS = TransformObjectToWorldNormal(IN.normalOS);
                float3 lightDir = normalize(_MainLightPosition.xyz);
                half ndotl = saturate(dot(normalWS, lightDir));
                half3 ambient = SampleSH(normalWS);
                OUT.lighting = ndotl * _MainLightColor.rgb + ambient;

                // Vertex Specular
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
                float3 H = normalize(lightDir + viewDir);
                half spec = saturate(dot(H, normalWS));
                spec = pow(spec, _SpecPower);
                OUT.specColor = spec * _SpecColor.rgb;

                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                color.rgb *= IN.lighting;
                color.rgb += IN.specColor;

                return color;
            }
            ENDHLSL
        }
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
    }
}

블린 퐁 셰이더와 비교해보면 half4 frag 프래그먼트 쪽에 있던 연산들이 Varings vert 버텍스 연산으로 옮겨진 것을 비교할 수 있다.

Blinn Phong / Gouraud Shading

 

 

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
                half3 lighting      : COLOR0;
                half3 specColor     : COLOR1;
            };

앞선 블린 퐁 연산에서 Varyings 구조체는 normal, lightDir, viewDir 등 라이팅 계산을 위한 재료들이 전달되었다. 하지만 버텍스 셰이더에서 라이팅 계산을 할 경우 프래그먼트 셰이더로 전달되는 값들은 라이팅의 결과물이 전달된다. specColor는 프래그먼트 셰이더에서 텍스쳐 샘플링 이후 더해야 해서 lighting과는 개별적으로 전달된다.

 

 

            // Lambert 라이팅 연산을 대부분 버택스 셰이더에서 수행
            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                float3 normalWS = TransformObjectToWorldNormal(IN.normalOS);
                float3 lightDir = normalize(_MainLightPosition.xyz);
                half ndotl = saturate(dot(normalWS, lightDir));
                half3 ambient = SampleSH(normalWS);
                OUT.lighting = ndotl * _MainLightColor.rgb + ambient;

램버트 라이팅에 Ambient 환경광과 라이트 컬러를 적용하는 부분이다.  프래그먼트 셰이더 계산 방식과 다르지 않다. 라이팅 결과는 OUT.lighting 변수에 기록되어 프래그먼트 셰이더로 전달된다. 전달 과정에서 보간기에 의해 픽셀마다 보간 된 값을 전달한다.

 

 

                // Vertex Specular
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
                float3 H = normalize(lightDir + viewDir);
                half spec = saturate(dot(H, normalWS));
                spec = pow(spec, _SpecPower);
                OUT.specColor = spec * _SpecColor.rgb;

버텍스 셰이더에서 블린 퐁 스페큘러를 계산한다. OUT.specColor 변수를 통해 보간되어 프래그먼트 셰이더로 전달된다.

 

 

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                color.rgb *= IN.lighting;
                color.rgb += IN.specColor;

                return color;
            }

버텍스 셰이더에서 전달받은 라이팅 값을 단순히 곱하고 더한다.

 

 

프래그먼트 셰이더에서 수행할 연산을 버텍스로 가져와서 작성하면 전달 구조체의 멤버도 혼란스러워지고 전체적인 셰이더 코드의 일관성도 흐트러지는 결과가 많기 때문에 잘 생각해서 작업해야 한다.

 

 


참고자료

 

아티스트를 위한 유니티 URP 셰이더 입문 - YES24

셰이더(Shader)를 HLSL(High Level Shader Language)로 다루고 싶은 아티스트를 위한 입문서다. 아티스트들의 눈높이에 맞추어 기초 그래픽스 이론과 셰이더 개념을 설명하고, 이를 유니티 엔진에서 활용할

www.yes24.com

728x90
반응형