Computer Graphics/ShaderPixel

ShaderPixel - 13. 3d shader (water block) 추가

surkim 2024. 11. 23. 16:07

과제의 "A 3D (or 4D) shader of your choice." 항목을 구현하기 위해 물 블록 효과를 추가하였다.

이번 구현은 구름에서 사용되었던 Signed Distance Field(SDF)를 활용하여, 물결치는 블록과 같은 동적 효과를 구현하였다. 특히, 2D 텍스처를 활용한 굴절 효과와 큐브맵을 활용한 반사 효과가 핵심이다.


Vertex Shader

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

uniform mat4 uView;
uniform mat4 uProjection;
uniform mat4 uTransform;

out mat4 inverseView;
out mat4 inverseProjection;
out vec2 texCoord;

void main() {
    inverseProjection = inverse(uProjection);
    inverseView = inverse(uView);
    texCoord = aTexCoord;
    vec4 clipPos = uTransform * vec4(aPos, 1.0);
    gl_Position = clipPos;
}

버텍스 셰이더는 cloud의 버텍스 셰이더를 가져왔다.


Fragment Shader

1. 광선 방향 계산

vec3 calculateRayDirection(vec2 fragCoord) {
    vec4 clipSpacePos = vec4((fragCoord / uResolution) * 2.0 - 1.0, -1.0, 1.0);    
    vec4 viewSpacePos = inverseProjection * clipSpacePos;
    viewSpacePos = vec4(viewSpacePos.xy, -1.0, 0.0);
    vec3 worldSpaceDir = normalize((inverseView * viewSpacePos).xyz);
    return worldSpaceDir;
}

2. 노이즈와 FBM(Fractal Brownian Motion)

mat3 m = mat3( 0.00,  0.80,  0.60,
              -0.80,  0.36, -0.48,
              -0.60, -0.48,  0.64);

float hash(float n)
{
    return fract(sin(n) * 43758.5453);
}

float noise(in vec3 x)
{
    vec3 p = floor(x);
    vec3 f = fract(x);
    
    f = f * f * (3.0 - 2.0 * f);
    
    float n = p.x + p.y * 57.0 + 113.0 * p.z;
    
    float res = mix(mix(mix(hash(n +   0.0), hash(n +   1.0), f.x),
                        mix(hash(n +  57.0), hash(n +  58.0), f.x), f.y),
                    mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
                        mix(hash(n + 170.0), hash(n + 171.0), f.x), f.y), f.z);
    return res;
}

float fbm(vec3 p) {
    float f;
    p += vec3(0.0, uTime * 0.3, uTime * 0.4); // 시간에 따라 위치를 변화
    f  = 0.5000 * noise(p); p = m * p * 2.02;
    f += 0.2500 * noise(p); p = m * p * 2.03;
    f += 0.1250 * noise(p);
    return f;
}
  • 물체 표면에 자연스러운 물결 효과를 생성하기 위해 FBM(프랙탈 브라운 운동)을 사용했다. 시간이 흐름에 따라 동적으로 변형된다.

3. SDF(Sign Distance Function): 정육면체

float sdBox(vec3 p, vec3 b) {
    p -= uCenter;
    vec3 d = abs(p) - b; // 점과 박스 표면 간의 거리
    float dist = length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
    float noise = fbm(p);
    return dist + 0.1 * noise;
}
  • FBM 노이즈를 추가하여 표면이 꿈틀대는 정육면체의 서명 거리 함수를 정의했다. 점 p와 박스의 경계면 사이의 거리를 계산한다.

4. Ray Marching

bool raymarch(vec3 rayOri, vec3 rayDir, out vec3 hitPos) {
    const float MAX_DISTANCE = 100.0;
    const float MIN_HIT_DISTANCE = 0.001;
    const int MAX_STEPS = 100;

    float totalDistance = 0.0;

    for (int i = 0; i < MAX_STEPS; ++i) {
        vec3 currentPos = rayOri + totalDistance * rayDir;
        float dist = sdBox(currentPos, vec3(1.0));

        if (dist < MIN_HIT_DISTANCE) {
            hitPos = currentPos;       
            return true;               
        }

        totalDistance += dist;         

        if (totalDistance > MAX_DISTANCE) {
            break;
        }
    }

    hitPos = vec3(0.0);
    return false;
}
  • 이제 익숙한 항상 쓰는 그것

5. 법선 계산

vec3 calculateNormal(vec3 p) {
    const vec3 epsilon = vec3(0.001, 0.0, 0.0);
    float dx = sdBox(p + epsilon.xyy, vec3(1.0)) - sdBox(p - epsilon.xyy, vec3(1.0));
    float dy = sdBox(p + epsilon.yxy, vec3(1.0)) - sdBox(p - epsilon.yxy, vec3(1.0));
    float dz = sdBox(p + epsilon.yyx, vec3(1.0)) - sdBox(p - epsilon.yyx, vec3(1.0));
    return normalize(vec3(dx, dy, dz));
}
  • 설명: 충돌 지점에서의 표면 법선을 계산한다. 이 값은 반사와 굴절 효과에 사용된다.

6. 반사와 굴절 효과

void main() {
    vec4 pixel = texture(tex, texCoord);
    if (pixel.a < 0.01) discard;

    vec3 hitPos;
    vec3 rayDir = calculateRayDirection(gl_FragCoord.xy);
    vec3 rayPos = uViewPos;

    if (raymarch(rayPos, rayDir, hitPos)) {

        vec3 normal = calculateNormal(hitPos);

        vec3 reflectedDir = reflect(rayDir, normal);
        vec3 reflectionColor = texture(cubeTex, reflectedDir).rgb;

        float refractionIndex = 1.0 / 1.33;
        vec3 refractedDir = refract(rayDir, normal, refractionIndex);
        vec3 refractedColor = texture(tex, texCoord + refractedDir.xy * 0.1).rgb;

        vec3 finalColor = mix(refractedColor, reflectionColor, 0.5);
        fragColor = vec4(finalColor, 1.0);

    } else {
        fragColor = pixel;
    }
}
  • 반사: reflect를 사용해 큐브맵에서 반사 색상을 가져온다.
  • 굴절: refract를 사용해 굴절된 텍스처 색상을 샘플링한다.
  • 최종 혼합: 반사와 굴절 색상을 혼합하여 자연스러운 물 효과를 구현한다.

결과

 

 

 

이로서 과제에서 요구하는 8개의 모든 오브젝트를 전부 구현하였다.

•  A mandelbox, with lights (shadows are optionals).

• A 3D Fractal of your choice (other than the mandelbox), with light, surrounding occlusion and shadows.

• An IFS.

• A transluscent object (using volumetric ray-marching) with non-volumetric diffuse and specular lighting on its surface (like colored glass marble). Think to implement a mode to diplay one the specular/diffuse term of the lighting to verify that your lighting works.

• A volumetric cloud with a volumetric light, plus an object (volumetric too) inside with a higher density. The denser object must cast (volumetric) shadows on the cloud (i.e you must manage the light absorption inside the participating media). Example for the cloud, without the object.

• A shader applied on a surface, evoking "another world" in 3D, quite like a portal or a window. Pay attention to the perspective, the rendering must like a window in real life.

• A 2D shader of your choice that uses a renderbuffer.

• A 3D (or 4D) shader of your choice.

이제 할 일은 코드 리펙토링과 오브젝트 그리는 순서를 조정해주는 것!

드디어 끝이 보인다..