성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>Unity로 실습하는 Shader (2) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model)</h1> <p> Shader를 Texture 없이 한번 작성해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Shader "My/gouraudShader" { Properties { } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // fixed4 color = fixed4(1,0,0,1); fixed4 color = 0; <span style='color: blue; font-weight: bold'>color.r = 1;</span> return color; } ENDCG } } } </pre> <br /> texture 없이 pixel shader에서 컬러 값을 R 성분만 줬기 때문에 빨간색으로 칠해질 텐데요, 컬러 값을 Inspector 창에서 넘겨 줄 수 있도록 다음과 같이 변경하겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Shader "My/gouraudShader" { Properties { <span style='color: blue; font-weight: bold'>_Color("Main Color", Color) = (1.0, 0.0, 0.0, 1.0)</span> } SubShader { Pass { CGPROGRAM // ...[생략]... <span style='color: blue; font-weight: bold'>float4 _Color;</span> fixed4 frag (v2f i) : SV_Target { <span style='color: blue; font-weight: bold'>fixed4 color = _Color;</span> return color; } ENDCG } } } </pre> <br /> 위의 shader를 적용한 Sphere와 적용 전의 Sphere를 보면 다음과 같습니다.<br /> <br /> <img alt='gouraud_light_1.png' src='/SysWebRes/bbs/gouraud_light_1.png' /><br /> <br /> 좌측의 것이 우리가 만든 shader를 적용한 것으로 Unity Editor에서는 Material에서 기본적으로 흰색 값을 넣어주기 때문에 저렇게 흰색으로 보이는 것입니다.<br /> <br /> 문제는, 완전히 흰색이라 전혀 3D 객체 같은 느낌이 안 난다는 점입니다. 그러니까, 명암 처리를 해줘야 한다는 것입니다. 바로 그 명암 처리를 Vertex Shader에서 해줄 텐데, 방법은 조명이 비치는 방향과 Sphere의 폴리곤들이 면을 이루는 방향(법선)을 코사인 함수로 처리하는 "<a target='tab' href='http://iskim3068.tistory.com/76'>램버트의 코사인 법칙</a>"을 이용합니다.<br /> <br /> 자, 그럼 이를 위해서 우선 법선의 방향을 알아야 하는데 이는 보통 Model에서 제공해 줍니다. 유니티의 경우 Sphere 객체에 법선 정보가 들어 있고, 따라서 그냥 Vertex Shader의 입력으로 변수만 추가해 주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > struct appdata { float4 vertex : POSITION; <span style='color: blue; font-weight: bold'>float3 normal : NORMAL;</span> }; </pre> <br /> 그런데, 전달된 normal 벡터는 Model의 로컬 좌표계 기준의 방향입니다. 조명은 World 좌표계에서 Model이 놓여있는 것을 계산하는 것이 유효하므로 사용 전에 World 좌표계로 변환해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); <span style='color: blue; font-weight: bold'>float3 worldNormal = UnityObjectToWorldNormal(v.normal);</span> // 정규화까지 덤으로! return o; } </pre> <br /> <br /> 이제 그다음 알아야 할 것은 조명의 위치나 방향입니다. 일반적으로 조명의 위치든 방향이든 그 값을 Properties를 통해 받아야 하지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Properties { _Color("Main Color", Color) = (1.0, 0.0, 0.0, 1.0) <span style='color: blue; font-weight: bold'>_CameraPosition("Camera Position", Vector) = (0.0, 0.0, 0.0, 0.0)</span> } </pre> <br /> 모든 Material마다 이렇게 조명 좌표 또는 방향을 일일이 전달해주는 것은 매우 불편한 일인데요, Unity의 경우 이런 불편함을 <a target='tab' href='https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html'>_WorldSpaceLightPos0</a> 내장 변수로 전달해 주기 때문에 다음과 같이 간단하게 해결할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > float4 lightDir = normalize(_WorldSpaceLightPos0); </pre> <br /> _WorldSpaceLightPos0의 편리한 점이라면, 유니티의 기본 Scene에 있는 조명이 "Directional Light"이기 때문에 이런 종류의 전역 조명에서는 조명의 방향을 나타내는 vector 값으로 갖고 있다는 것입니다. 반면 조명 유형이 "Point Light"라면 조명의 위치를 점 데이터로 갖고 있습니다. (동차 좌표계이기 때문에 vector인 경우 z 성분이 0, 점 위치인 경우라면 z 성분이 1로 처리한 걸까요? 아니면 그냥 구분의 의미였을까요? ^^) 또한 이름에 "World"가 포함되어 있는 걸로 봐서 World 좌표계에서의 조명 방향(또는 위치)임을 알 수 있습니다.<br /> <br /> 자, 그럼 이제 준비가 되었습니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; <span style='color: blue; font-weight: bold'>float4 illumination : COLOR0;</span> }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 lightDir = normalize(_WorldSpaceLightPos0); <span style='color: blue; font-weight: bold'>o.illumination = saturate(dot(worldNormal, lightDir));</span> return o; } float4 _Color; fixed4 frag (v2f i) : SV_Target { <span style='color: blue; font-weight: bold'>float4 color = _Color * i.illumination;</span> return color; } </pre> <br /> 여기서 빠진 것이 있는데 바로 조명의 색에 대한 반영입니다. 조명 색상 역시 DirectX와 같은 프로그램이라면 외부에서 "Properties"를 이용해 전달해야겠지만, Unity의 경우 <a target='tab' href='https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html'>_LightColor0</a> 내장 변수로 제공하고 있으니 이를 사용하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>#include "Lighting.cginc"</span> v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 lightDir = normalize(_WorldSpaceLightPos0); o.illumination = <span style='color: blue; font-weight: bold'>_LightColor0</span> * saturate(dot(worldNormal, lightDir)); return o; } </pre> <br /> 그런데, 이렇게 조명 처리를 하고 다시 비교해 보면 이렇습니다.<br /> <br /> <img alt='gouraud_light_2.png' src='/SysWebRes/bbs/gouraud_light_2.png' /><br /> <br /> 오호~~~ 음영이 있으니 3D 객체 같은 느낌이 확실히 살아났습니다. <br /> <br /> <hr style='width: 50%' /><br /> <br /> 이제 많은 책에서 설명하고 있는 조명 공식을 보고 그에 맞게 적용해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 색 = 주변 조명 + 확산(diffuse) 조명 + 반사 조명 </pre> <br /> 이 중에서 우리가 지금까지 실습했던 것이 바로 "확산(diffuse) 조명"입니다. 여기에 World 전역적으로 밝기를 제공하는 "주변 조명"을 더할 수 있는데요, 간단합니다. 그냥 물체가 어느 정도 밝기였으면 좋겠다 싶은 값을 넣으면 됩니다. 단지, Unity의 경우 이 값을 모든 Shader에서 공유할 수 있도록 내장 변수로 제공해 주고 있으므로 이를 사용하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 주변광 = K * I (K는 주변 조명 계수로 임의의 상수, I 역시 전역적으로 설정한 주변 조명 밝기로 임의의 상수) float4 ambientReflection = 1.0 * <span style='color: blue; font-weight: bold'>UNITY_LIGHTMODEL_AMBIENT</span>; </pre> <br /> 위에서 주변 조명 계수라고 불리는 1.0을 곱했는데, 정해진 UNITY_LIGHTMODEL_AMBIENT 값 이외에 모델 개별적으로 조절할 수 있는 값이라고 보면 됩니다. 이 글에서는, 그냥 K 값을 변수 처리하지 않고 1.0으로 두고 쓰겠습니다.<br /> <br /> 주변 조명 계수가 있다면 당연히 확산(diffuse) 조명에 대한 계수도 있어야 합니다. 따라서, 우리가 사용했던 확산 조명의 크기를 다음과 같이 변경할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 확산광 = K * I (K는 확산 조명 계수로 임의의 상수, I는 Model과 빛의 방향에 따라 계산한 확산 조명 값) float4 diffuseReflection = <span style='color: blue; font-weight: bold'>1.0</span> * _LightColor0 * saturate(dot(worldNormal, lightDir)); </pre> <br /> 일단, 여기까지 해서 "주변 조명" + "확산 조명"한 결과를 적용해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > struct v2f { float4 vertex : SV_POSITION; float4 illumination : COLOR0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); <span style='color: blue; font-weight: bold'>float4 ambientReflection = 1.0 * UNITY_LIGHTMODEL_AMBIENT;</span> float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 lightDir = normalize(_WorldSpaceLightPos0); <span style='color: blue; font-weight: bold'>float4 diffuseReflection</span> = <span style='color: blue; font-weight: bold'>1.0 * </span>_LightColor0 * saturate(dot(worldNormal, lightDir)); <span style='color: blue; font-weight: bold'>o.illumination = ambientReflection + diffuseReflection;</span> return o; } </pre> <br /> 반영하면 이렇게 나옵니다.<br /> <br /> <img alt='gouraud_light_3.png' src='/SysWebRes/bbs/gouraud_light_3.png' /><br /> <br /> 오호~~~ 주변광만을 더했을 뿐인데 많이 근사해졌습니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이제 마지막으로 반사 조명(specular illumination)을 반영해 보겠습니다. 반사 조명은, 일반적으로 빛의 물체에 대한 입사각과 동일한 반사각일 때 가장 큽니다. 바로 그 반사각에 카메라가 있다면 반사 조명의 밝기는 더 클 것이고 그 반사각에 비켜 있을수록 밝기는 작아지게 됩니다. 게다가 또 하나 영향을 주는 것이 바로 물체의 "표면"입니다. 표면이 거울처럼 매끄럽다면 보다 많은 빛을 반사할 것이고, 그렇지 않다면 반사되는 빛의 양이 적을 것입니다.<br /> <br /> 따라서 반사광의 크기는 다음과 같이 정해집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 확산광 = K * (반사각과 카메라가 바라보는 각에 따른 반사광 강도)<sup>n</sup> // K == 반사 조명 계수 (1.0으로 처리) // 반사각 == 모델의 법선 벡터를 기준으로 빛의 입사각을 반사 float3 reflectedDir = reflect(-lightDir, worldNormal); // reflect 함수는 법선에 대한 입사각의 반사각을 구해줌. // 카메라가 바라보는 각 == (카메라의 월드 위치 - 모델의 월드 위치) float3 viewDir = normalize(_WorldSpaceCameraPos - worldNormal); // (반사각과 카메라가 바라보는 각에 따른 반사광 강도) float reflectIntensity = saturate(dot(reflectedDir, viewDir)); // n == 표면 반사 적용할 상수 float n = 4.0; reflectIntensity = pow(reflectIntensity, n); // 최종 반사광 float3 specularReflection = 1.0 * _LightColor0 * reflectIntensity; </pre> <br /> 위와 같이 반사광을 계산하는 것을 "퐁 모델(Phong model)"이라 부른다고 합니다.<br /> <br /> 따라서, 3가지 종류의 빛을 모두 합산해 적용하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > o.illumination = float4(ambientReflection + diffuseReflection + specularReflection, 1.0); </pre> <br /> 이제 다음과 같은 모습을 볼 수 있습니다.<br /> <br /> <img alt='gouraud_light_4.png' src='/SysWebRes/bbs/gouraud_light_4.png' /><br /> <br /> 빛의 세기에 따라 어떻게 외양이 변하는지 K 계수 값을 조절해 보시면 됩니다. 가령, 주변광의 계수를 1.0에서 2.0으로 높이니 그나마 Unity가 제공하는 "Default-Material"의 "Standard" shader와 유사하게 외양이 나옵니다.<br /> <br /> <img alt='gouraud_light_5.png' src='/SysWebRes/bbs/gouraud_light_5.png' /><br /> <br /> 아래는 이를 모두 적용한 shader 코드입니다.<br /> <br /> <pre style='height: 400px; margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Shader "My/gouraudShader" { Properties { _Color("Main Color", Color) = (1.0, 1.0, 1.0, 1.0) } SubShader { Pass { <a target='tab' href='http://www.sysnet.pe.kr/2/0/11642'>Tags{ "LightMode" = "ForwardBase" }</a> CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float4 illumination : COLOR0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); // 주변광 float4 ambientReflection = 2.0 * UNITY_LIGHTMODEL_AMBIENT; // 확산광 float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 lightDir = normalize(_WorldSpaceLightPos0); /* float4 _WorldSpaceLightPos0; */ float4 diffuseReflection = 1.0 * _LightColor0 * saturate(dot(worldNormal, lightDir)); // 반사광 float3 reflectedDir = reflect(-lightDir, worldNormal); float3 viewDir = normalize(_WorldSpaceCameraPos - worldNormal); /* float3 _WorldSpaceCameraPos; */ float reflectIntensity = saturate(dot(reflectedDir, viewDir)); float n = 4.0; reflectIntensity = pow(reflectIntensity, n); float3 specularReflection = 1.0 * _LightColor0 * reflectIntensity; o.illumination = float4(ambientReflection + diffuseReflection + specularReflection, 1.0); return o; } float4 _Color; fixed4 frag (v2f i) : SV_Target { float4 color = _Color * i.illumination; return color; } ENDCG } } } </pre> <br /> 이제까지 만든 shader가, 정점(vertex) 단위에서 수행한 "Phong Model"의 "고로 셰이딩"입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, 제 예제에서 생략한 외부 변수 처리들에 대한 것들이 모두 제대로 구현된 gouraud shader를 아래의 사이트에서 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > psicomante/gouraud.shader ; <a target='tab' href='https://gist.github.com/psicomante/2109b947a681804f8ba5625238895a2a#file-gouraud-shader-L70'>https://gist.github.com/psicomante/2109b947a681804f8ba5625238895a2a#file-gouraud-shader-L70</a> </pre> <br /> 어찌 보면, 외부 변수 처리들이 shader 프로그램할 때 꽤나 귀찮은 작업들의 하나입니다. 그래도 Unity는 이에 대한 불편함을 상당히 개선한 것인데요, 일례로 저 모든 변수들을 DirectX 등에서 코딩으로 직접 넣어주는 것은 상상만 해도?!!!! ^^;<br /> <br /> <hr style='width: 50%' /><br /> <br /> 보통 Model에서 법선 정보를 갖고 있다고 했는데요, 만약 없다면 어떻게 해야 할까요? 가장 쉬운 것은 Vertex Shader 함수에 전달된 Vertex가 삼각형 Polygon을 이루는 인접 Vertex 들의 정보를 알 수 있어야 합니다. 그렇게만 되면 인접 Vertex들의 좌표를 이용해 면의 vector 2개를 얻고 그것을 외적해 normal vector를 얻을 수 있기 때문입니다. 아쉬운 것은, vertex shader에서 인접 폴리곤들의 vertex를 알 수 있는 방법이 없다는 것입니다. 아래의 글을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Calculate Normals In Shader ; <a target='tab' href='http://tonfilm.blogspot.com/2007/01/calculate-normals-in-shader.html'>http://tonfilm.blogspot.com/2007/01/calculate-normals-in-shader.html</a> </pre> <br /> 그래서, 법선 벡터를 구하기 위해 CPU 레벨에서, 즉 DirectX 같으면 shader 돌리기 전에 법선 벡터를 구해야 한다는 것입니다. 그나마 좋은 소식이라면, DirectX 11부터 지원하는 Shader Model 4부터는 기하 셰이더(geometry shader)가 제공되므로 거기서 인접 폴리곤들의 위치를 알 수 있으므로 계산할 수 있다고 합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위에서 사용한 UnityObjectToWorldNormal 함수는 Unity에서 제공하는 도우미이고, DirectX와 같은 환경에서 계산한다면 다음과 같이 World 행렬을 이용해 변환 후 정규화해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > half3 worldNormal = mul(unity_ObjectToWorld, v.normal); worldNormal = normalize(worldNormal); </pre> <br /> 그런데 자세히 보면 unity_ObjectToWorld가 float4x4인데, half3인 v.normal을 곱하는 것이 왠지 에러가 날 듯싶은데, mul 함수가 이를 지원하기 때문에 유효합니다. 물론, 명시적으로 half3을 half4로 확장해서 연산해도 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > half4 modelNormal = half4(v.normal, 0); half3 worldNormal = mul(unity_ObjectToWorld, modelNormal); </pre> <br /> 다시, float4x4 * half4의 결과로 half3이 나오는 것이 이상하지만 shader 측에서 이를 지원하는 것 같습니다. 단지, 이것도 맞추고 싶다면 다음과 같이 연산해도 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); // 이 한줄의 코드 대신, // half3 worldNormal = UnityObjectToWorldNormal(v.normal); // 다음과 같이 풀어서 사용하는 것도 가능. half4 modelNormal = half4(v.normal, 0); half4 worldNormal = normalize(mul(unity_ObjectToWorld, modelNormal)); o.worldNormal = worldNormal.xyz; return o; } </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
3671
(왼쪽의 숫자를 입력해야 합니다.)