[2023 동계 모각소] 알구자구 3주차

jungizz_·2024년 1월 25일
0

모각소

목록 보기
9/12
post-thumbnail

📝 3주차

  • PBR(BRDF + Light)
  • Remapping roughness
  • Gamma correction
  • Normal mapping(min-map)

1. Physically-based Rendering

🔗 PBR engine Filament

BRDF

  • Bidirectional Reflectance Distribution Function
  • Incident light가 어떤 방향으로 반사가 되는가
    • diffuse reflectance fdf_d
    • specular reflectance frf_r
    • f(v,l)=fd(v,l)+fr(v,l)f(v,l)=f_d(v,l)+f_r(v,l)
  • 실제로는 일부 incient light는 표면을 투과하여 분산되고, 다시 표면을 나와 diffuse reflectance를 형성
  • specular reflectance + diffuse reflectance energy < incident energy

Metallic(conductor)

  • no subsurface scattering
  • no diffuse component

non-Metallic(dielectric)

  • subsurface scattering
  • have specular and diffuse components

Specular BRDF

  • fr(v,l)=D(h,α)G(v,l,α)F(v,h,f0)4(nv)(nl)f_r(v,l)=\frac{D(h,α)G(v,l,α)F(v,h,f0)}{4(n⋅v)(n⋅l)}

1. Normal distribution function (specular D)

  • long-tailed normal distribution functions이 실제 표면에 적절 -> GGX 모델 사용
  • DGGX(h,α)=α2π((nh)2(α21)+1)2D_{GGX}(h,α)=\frac{α^2}{π((n⋅h)^2(α^2−1)+1)^2}
#define MEDIUMP_FLT_MAX    65504.0
#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)

float D_GGX(float roughness, float NoH, const vec3 n, const vec3 h) {
    vec3 NxH = cross(n, h);
    float a = NoH * roughness;
    float k = roughness / (dot(NxH, NxH) + a * a);
    float d = k * k * (1.0 / PI);
    return saturateMediump(d);
}

2. Geometric shadowing (specular G)

  • V(v,l,α)=G(v,l,α)4(nv)(nl)V(v,l,α)=\frac{G(v,l,α)}{4(n⋅v)(n⋅l)}
  • V(v,l,α)=0.5nl(nv)2(1α2)+α2+nv(nl)2(1α2)+α2V(v,l,α)=\frac{0.5}{n⋅l\sqrt{(n⋅v)^2(1−α^2)+α^2}+n⋅v\sqrt{(n⋅l)^2(1−α^2)+α^2}}
float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
    float a2 = roughness * roughness;
    float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
    float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
    return 0.5 / (GGXV + GGXL);
}

3. Fresnel (specular F)

  • 반사되는 빛의 양은 보는 방향(각도)과 IOR(index of refraction; 굴절률)에 따라 다르다
  • 표면과 수직인 각도일수록 적은 반사, 이때 굴절률은 f0f_0
  • 표면과 평행한 각도일수록 많은 반사, 이때 굴절률은 f90f_{90}
  • FSchlick(v,h,f0,f90)=f0+(f90f0)(1vh)5F_{Schlick}(v,h,f_0,f_{90})=f_0+(f_{90}−f_0)(1−v⋅h)^5
vec3 F_Schlick(float u, vec3 f0, float f90) {
    return f0 + (vec3(f90) - f0) * pow(1.0 - u, 5.0);
}

Fresnel Reflectance value

f0f_0

  • specular reflectance at normal incidence angle
  • non-metallic에선 색이 없고, metallic에선 색이 있음

f90f_{90}

  • fresnel reflectance at grazing angle
  • All materials have a Fresnel reflectance of 100% at grazing angles (f90f_{90}=1.0)
  • acceptable Fresnel reflectance(f0f_0) values for various types of materials (no real world material has a value under 2%)
f0 = vec3(0.028);
f90 = 1;

Diffuse BRDF

  • f0f_0f90f_{90}의 interpolating
  • 실제 material은 non-metallic과 metallic 둘 다 grazing angles에서 무색의 specular reflectance를 나타냄
  • 실제 material은 fresnel reflectance는 90도에서 1
vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic;
  1. simple diffuse Lambertian BRDF
    • assume uniform diffuse response over the microfacets hemisphere
    • σ: diffuse reflectance
    • fd(v,l)=σπf_d(v,l)=\frac{σ}{π}
  2. higher quality Disney diffuse BRDF
    • roughness 고려
    • grazing angle의 retro-reflection 생성 (아래 우측 구의 left edge)
    • fd(v,l)=σπFSchlick(n,l,1,f90)FSchlick(n,v,1,f90)f_d(v,l)=\frac{σ}{π}F_{Schlick}(n,l,1,f_{90})F_{Schlick}(n,v,1,f_{90})

✔️ main.cpp

  • light 위치, 색상 추가
  • fragment shader에 전달
. . .

vec3 lightPosition = vec3(3, 3, 10);
vec3 lightColor = vec3(500)

. . .

void render(GLFWwindow* window) 
{  
	. . .
    
    GLuint lightPositionLocation = glGetUniformLocation(program.programID, "lightPosition");
    glUniform3fv(lightPositionLocation, 1, value_ptr(lightPosition));

    GLuint lightColorLocation = glGetUniformLocation(program.programID, "lightColor");
    glUniform3fv(lightColorLocation, 1, value_ptr(lightColor));
    
    . . .
}

✔️ shader.frag

  • dot product한 결과는 clamp를 사용하여 [0, 1] 범위로 지정
  • specular BRDF와 diffuse BRDF 계산
. . .

float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
    float a2 = roughness * roughness;
    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
    return 0.5 / (GGXV + GGXL);
}

float D_GGX(float NoH, float roughness) {
    float a2 = roughness * roughness;
    float f = (NoH * a2 - NoH) * NoH + 1.0;
    return a2 / (PI * f * f);
}

vec3 F_Schlick(float u, vec3 f0) {
    return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0);
}

float Fd_Lambert(){
	return 1.0 / PI;
}

void main(void)
{
	vec3 L = lightPosition - worldPosition;				// light unit vector
	vec3 l = normalize(L);							    // light unit vector
	vec3 n = normalize(normal);							// normal unit vector
	vec3 v = normalize(cameraPosition - worldPosition); // view unit vector
	vec3 h = normalize(l+v);							// half unit vector

	float NoV = abs(dot(n, v)) + 1e-5;
	float NoL = clamp(dot(n, l), 0.0, 1.0);
	float NoH = clamp(dot(n, h), 0.0, 1.0);
	float LoH = clamp(dot(l, h), 0.0, 1.0);
	
	// BRDF
	vec3 f0 = vec3(0.028); // 'skin' specular reflectance at normal incidnece angle
	float roughness = texture(roughTex, texCoords).r;

	// 1. specular BRDF
	float D = D_GGX(NoH, NoH * roughness);
	float V = V_SmithGGXCorrelated(NoV, NoL, roughness);
	vec3 F = F_Schlick(LoH, f0);

	vec3 Fr = (D * V) * F;

	// 2. diffuse BRDF
	vec4 diffColor = texture(diffTex, texCoords);
	vec3 Fd = diffColor.rgb * Fd_Lambert();
	
	// final
	out_Color.xyz = Fd + Fr);
}

Roughness remapping

  • 유저가 설정한 roughness값(또는 roughness Texture)을 그대로 사용하면 rough범위가 좁아 활용 범위가 적어짐
  • real world같은 rough범위를 만들기 위해 유저가 설정한 roughness 값을 제곱한 알파 값을 사용 (실제로 보통 알파라고 칭함)
  • α=perceptualRoughness2α=perceptualRoughness^2(위-알파값 사용, 아래-roughness값만 사용)

✔️ shader.frag

...

vec3 f0 = vec3(0.028); 
float roughness = texture(roughTex, texCoords).r;
roughness *= roughness; // remapping roughness (alpha)

...

light 추가

  • PRB 식에서 밑줄친 Li(x,ωi)(ωi,n)L_i(x, ω_i)(ω_i, n)부분을 추가

✔️ shader.frag

... 

// final
vec3 c = (Fd + Fr) * (lightColor/dot(L, L)) * NoL;
out_Color.xyz = c;


2. Gamma Correction

  • 모니터는 비선형, 렌더러는 선형
  • 이미지는 SRGB이고, 렌더러는 linear space

to linear space

  • 이미지(ex-diffuse texture)를 렌더러로 가져와서 계산할 때, 이미지를 γ=2.2로 Gamma correction해서 linear space로 변형 (두 가지 방법 존재)
    ⅰ. shader code에서 받은 텍스쳐 값에 직접 gamma correction
    // diffuse BRDF
    vec4 diffColor = texture(diffTex, texCoords);
    diffColor.rgb = pow(diffColor.rgb, vec3(2.2)); // gamma correction (to linear space)
    vec3 Fd = diffColor.rgb * Fd_Lambert();
    ⅱ. 이미지 읽어올 때 GL_SRGB8_ALPHA8로 읽어오기
    // Texture
    // load diffuse map
    int w, h, n;
    void* buf = stbi_load("LPS_lambertian.jpg", &w, &h, &n, 4);
    glGenTextures(1, &diffTex);
    glBindTexture(GL_TEXTURE_2D, diffTex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf); // gamma correction (to linear space)
    stbi_image_free(buf);
    • dielectric material(피부 등)은 색 없이 빛을 반사하므로 PBR skin shader는 white specular color를 가짐
      -> 근데 Gamma correction을 하지 않으면 전반적으로 노란 specular color를 갖게 됨!

to SRGB

  • 렌더러에서 계산한 최종 색상 값을 모니터로 렌더링할 때, 최종 색상 값을 γ=1/2.2로 Gamma correction해서 SRGB로 변형
    // final
    vec3 c = (최종 색상)
    out_Color = vec4(pow(c, vec3(1/2.2)), diffColor.a); // gamma correction (to srgb)

3. Normal mapping

✔️shader.frag

  • Normal map 텍스쳐로부터 읽어온 normal의 범위를 [-1~1]로 확장
  • 읽어온 normal에 TBN행렬을 곱하여 변환
. . .

mat3 getTBN(vec3 N){
	vec3 Q1 = dFdx(worldPosition), Q2 = dFdy(worldPosition); // wourldpos 미분 (compute tangent, bitangent)
	vec2 st1 = dFdx(texCoords), st2 = dFdy(texCoords);		 // texCoord 미분 
	float D = st1.s*st2.t - st1.t*st2.s;
	return mat3(normalize((Q1*st2.t - Q2*st1.t)*D), 
				normalize((-Q1*st2.s + Q2*st1.s)*D), 
				N);
}

void main(void)
{
	vec3 L = lightPosition - worldPosition;				// light unit vector
	vec3 l = normalize(L);							    // light unit vector
	vec3 n = normalize(normal);							// normal unit vector
	vec3 v = normalize(cameraPosition - worldPosition); // view unit vector
	vec3 h = normalize(l+v);							// half unit vector

	// normal mapping
	mat3 TBN = getTBN(n);
	vec3 normVec = texture(normTex,texCoords).rgb*2-1; // [0, 1] -> [-1, 1]
	n = normalize(TBN * normVec);
	
    // BRDF
    . . .
    

Mip-Map

  • 모델이 작아질 때 패턴이 깨지는 문제를 보완하기 위해 mip-mapping으로 미리 평균 구하기
  • 텍스쳐 크기를 줄여가며 미리 texel 평균을 구해두고, 모델이 작아졌을 때 미리 구한 작은 텍스쳐의 평균을 사용해 mapping

✔️main.cpp

  • 텍스쳐 로드할 때, mip map 생성
GLuint loadTextureMap(const char* filename)
{
    int w, h, n;
    GLuint texID;
    void* buf = stbi_load(filename, &w, &h, &n, 4);
    glGenTextures(1, &texID);
    glBindTexture(GL_TEXTURE_2D, texID);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); // GL_LINEAR_MIPMAP_LINEAR: 축소되었을 때 적당한 것 찾기
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf);
    glGenerateMipmap(GL_TEXTURE_2D); // 자동으로 mipmap 생성
    stbi_image_free(buf);
    return texID;
}


🔎 구현 과정에서...

fresnel reflectance value

  • F_schlick 식에서는 f0을 vec3로 받는데, 금속 물질은 vec3으로 나타나있는데 피부같은 비금속의 f0은 스칼라로 나타나있어 어떻게 사용하는지 몰랐음
    → 그냥 vec3(0.028, 0.028, 0.028) 사용는 것임

gamma correction

  • 계산 다 하고, 최종 색상 값에만 gamma correction을 하여 렌더링했는데, 모델의 색상이 회색빛이 도는 문제
    -> diffuse texture에 gamma correction을 하지 않아 생긴 문제
  • SRGB와 linear space를 이해하고, gamma correction의 모호했던 개념을 정확히 짚고 넘어갈 수 있었음

light 추가

  • (specualr BRDF + diffuseBRDF)에 light 추가하고 너무 밝아지는거 보완하기 위해 gamma correction했는데 요상하게 나온당
    → dot product하고 clamp안해서;;ㅎㅎ
vec3 c = (Fd + Fr) * (lightColor/dot(L, L)) * dot(n, l); //가 아니라
vec3 c = (Fd + Fr) * (lightColor/dot(L, L)) * NoL;       //(float NoL = clamp(dot(n, l), 0.0, 1.0);)

❗벡터 곱할때 *가 아니라 dot product!! 바보

profile
( •̀ .̫ •́ )✧

0개의 댓글