그림자 이펙트 구현.
2 pass 알고리즘이라고도 불리는 그림자 이펙트는
구현하기 위해서는 픽셀마다 ray marching 2번 수행해야 한다.
카메라 원점에서 Ray Marching을 해서 물체와 충돌되는 지점 (p1) 를 찾았다면, --> 1
다시 p1 지점에서 p1 --> light position 방향으로 Ray Marching을 다시 진행한다. --> 2
이때 p1 지점과 light position 간 거리도 구해 놓는다. --> 3
2 번 ray marching 에서 얻은 뎁스 값과 3번에서 구한 거리 값을 비교해 본다.
뎁스 값이 더 작다면, 물체와 태양 사이 다른 오브젝트가 있는 것으로 이해할 수 있으며
그림자가 생성 된다.
뎁스 값이 같으면 물체와 태양 사이 아무것도 없는 것이며,
그림자가 생성되지 않는다.
위의 있는 예제는 floor 영역에 픽셀이 원의 대한 그림자 영역이 표시가 된다.
p1 지점에 " normal * EPSILON * 3 "을 더한 이유는 무엇일까 ?
ray marching 시작 지점이 카메라가 아니라 p1이며,
p1도 물체이기 때문에 아주 작은 값이라도 더하지 않으면 rayMarching 함수에서
리턴하기 때문.
그림자 코드 >>
// shadow
vec3 newRayOrigin = p + normal * EPSILON * 3.;
vec3 newRayDi = normalize(lightPos - newRayOrigin);
float shadowRayLength = rayMarch(newRayOrigin, newRayDi);
if (shadowRayLength < length(lightPos - newRayOrigin)) {
col = vec3(0.);
}
전체 코드 >>
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
float sdSphere(vec3 p, float r) {
return length(p) -r;
}
float sdFloor(vec3 p) {
return p.y + 1.;
}
float scene(vec3 p) {
float sphere = sdSphere(p, 1.);
float floors = sdFloor(p);
return min(sphere, floors);
}
float rayMarch(vec3 ro, vec3 rd) {
float d = MIN_DIST;
for (int i=0; i<MAX_MARCHING_STEPS; i++) {
vec3 p = ro + rd * d;
float depth = scene(p);
d += depth;
if (d < PRECISION || d > MAX_DIST) break;
}
return d;
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * scene(p + e.xyy) +
e.yyx * scene(p + e.yyx) +
e.yxy * scene(p + e.yxy) +
e.xxx * scene(p + e.xxx));
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-0.5*iResolution.xy) / iResolution.y;
vec3 background = vec3(0.);
vec3 col = background;
vec3 ro = vec3(0, 0, 5);
vec3 rd = normalize(vec3(uv, -1));
float d = rayMarch(ro, rd);
if (d < MAX_DIST) {
vec3 p = ro + rd * d;
vec3 lightPos = vec3(cos(iTime), 2, sin(iTime));
vec3 lightDi = normalize(lightPos - p);
vec3 diffuseCol = vec3(1.);
vec3 normal = calcNormal(p);
float diffuse = clamp(dot(normal, lightDi), 0., 1.);
col = diffuseCol * diffuse;
// shadow
vec3 newRayOrigin = p + normal * EPSILON * 3.;
vec3 newRayDi = normalize(lightPos - newRayOrigin);
float shadowRayLength = rayMarch(newRayOrigin, newRayDi);
if (shadowRayLength < length(lightPos - newRayOrigin)) {
col = vec3(0.);
}
}
fragColor = vec4(col,1.0);
}
< 그림자 1 > 을 보면 그림자의 경계선이 명확하여 어색해 보인다.
아래는 소프트 셰도우 적용 코드이다.
float softShadow = clamp(softShadow(p, ld, 0.02, 2.5), 0.1, 1.0);
col = s.col * diffuse * softShadow;
소프트 셰도우도 rayMarching 기법을 동일하게 사용하지만,
그림자 영역 계산하는 부분이
길이비교 if 문으로 나누어 지는 방식이 아니고, t 값으로 나눈 값을 사용 하는 것이 차이점.
float softShadow(vec3 ro, vec3 rd, float mint, float tmax) {
float res = 1.0;
float t = mint;
for(int i = 0; i < 16; i++) {
float h = scene(ro + rd * t).d;
res = min(res, 8.0*h/t);
t += clamp(h, 0.02, 0.10);
if(h < 0.001 || t > tmax) break;
}
return clamp( res, 0.0, 1.0 );
}
추가로 바닥의 경계면도 부드럽게 보이기 위하여 포그 이펙트를 적용해보자.
col = mix(col, background, 1.0 - exp(-0.0002 * s.d * s.d * s.d));
전체 코드 >
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
struct Surface {
float d;
vec3 col;
};
Surface sdSphere(vec3 p, float r) {
Surface s;
s.d = length(p) - r;
s.col = vec3(1, 0, 0);
return s;
}
Surface sdFloor(vec3 p) {
Surface s;
s.d = p.y + 1.;
s.col = vec3(mod(floor(p.x) + floor(p.z), 2.) * 0.7);
return s;
}
Surface scene(vec3 p) {
Surface a = sdSphere(p, 1.);
Surface b = sdFloor(p);
if (a.d < b.d) return a;
return b;
}
Surface rayMarch(vec3 ro, vec3 rd) {
float d = MIN_DIST;
Surface s;
for (int i=0; i<MAX_MARCHING_STEPS; i++) {
vec3 p = ro + rd * d;
s = scene(p);
d += s.d;
if (d < PRECISION || d > MAX_DIST) break;
}
s.d = d;
return s;
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * scene(p + e.xyy).d +
e.yxy * scene(p + e.yxy).d +
e.yyx * scene(p + e.yyx).d +
e.xxx * scene(p + e.xxx).d);
}
float softShadow(vec3 ro, vec3 rd, float mint, float tmax) {
float res = 1.0;
float t = mint;
for(int i = 0; i < 16; i++) {
float h = scene(ro + rd * t).d;
res = min(res, 8.0*h/t);
t += clamp(h, 0.02, 0.10);
if(h < 0.001 || t > tmax) break;
}
return clamp( res, 0.0, 1.0 );
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-0.5*iResolution.xy) / iResolution.y;
vec3 background = vec3(0.3);
vec3 col = background;
vec3 ro = vec3(0, 0, 5);
vec3 rd = normalize(vec3(uv, -1));
vec3 lightPos = vec3(cos(iTime), 2, sin(iTime));
Surface s = rayMarch(ro, rd);
if (s.d < MAX_DIST) {
vec3 p = ro + rd * s.d;
vec3 ld = normalize(lightPos - p);
vec3 normal = calcNormal(p);
float diffuse = clamp(dot(ld, normal), 0., 1.);
float softShadow = clamp(softShadow(p, ld, 0.02, 2.5), 0.1, 1.0);
col = s.col * diffuse * softShadow;
/*
// Hard Shadow
vec3 newRayOrigin = p + normal * PRECISION * 2.;
float sdepth = rayMarch(newRayOrigin, ld).d;
if (sdepth < length(lightPos - newRayOrigin)) {
col *= 0.1
}
*/
}
col = mix(col, background, 1.0 - exp(-0.0002 * s.d * s.d * s.d)); // fog
col = pow(col, vec3(1.0/2.2)); // Gamma correction
fragColor = vec4(col,1.0);
}
Reference
https://inspirnathan.com/posts/59-shadertoy-tutorial-part-13
Shadertoy Tutorial Part 13 - Shadows
Add shadows, gamma correction, and fog to your 3D scene!
inspirnathan.com
'Shader CG' 카테고리의 다른 글
10. RayMarching - Cube map (0) | 2024.08.13 |
---|---|
09. RayMarching - SD Operation (0) | 2024.08.12 |
07. RayMarching - Frenel effect (0) | 2024.08.10 |
06. RayMarching - 퐁 라이팅 모델 (0) | 2024.08.08 |
05. RayMarching - Camera move (0) | 2024.08.05 |