GLSL 지연된 렌더링을 통한 여러 빛 쉐이더 처리 및 화면 공간에서 광원 처리 - Whitmem
GLSL 지연된 렌더링을 통한 여러 빛 쉐이더 처리 및 화면 공간에서 광원 처리
Graphic Development
2025-01-15 21:07 게시 cc8f9d2311c1521a8b81

0
0
75
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
지연된 렌더링
지연된 렌더링을 통한 GBuffer을 통한 빛 연산 및 쉐이딩
한 개의 빛이 아닌 다량의 광원을 처리하기 위해서 기존 버텍스 쉐이더와 픽셀 쉐이더를 이용한 방법으로는 각각의 광원에 대해 해당 개수만큼 객체를 그려야한다. 광원이 3개 있다면, 각 3개마다 객체를 하나씩 그려야하며 이는 시간 복잡도를 늘려 렌더링 속도를 저하할 수 있다. 이를 해결하기 위해 객체의 모든 정보를 담는 텍스처를 생성한 뒤 해당 텍스처를 역으로 월드 공간으로 옮겨 각 필요한 빛 연산을 수행함으로써 적어도 픽셀 개수만큼만 빛 연산을 수행할 수 있고 필요 없는 연산을 최소화할 수 있으며, 여러 빛 처리를 섞어 처리할 수 있다.
지연된 렌더링을 처리하기 위해서는 먼저 GBuffer을 각각 생성해야 한다. 하나의 객체를 처리하기 위해서 하나의 객체에서 지오메트리 정보를 받아 처리하고, 행렬로 좌표계를 이동하고, 벡터를 처리해 픽셀 쉐이더 단에서 각 표면에 대해 빛 연산을 수행했다. 하지만 지연된 렌더링에서는 우선 객체의 모든 정보를 텍스처에 저장하고, 스크린 공간에서 월드 공간으로 이동하여 픽셀 개수만큼만 표면을 처리한다.
광원 2개로 합성 결과
위 이미지는 차례로 디퓨즈맵, Zbuffer을 포함한 지오메트리 맵, 노말 맵을 담고 있다. 이러한 정보를 마지막 포스트 프로세싱 전에 프로젝션 행렬과 뷰 행렬의 역 행렬을 통해 화면 공간에서 월드 공간으로 이동하고, 월드 공간에서 빛을 연산하여 각 연산 결과를 디퓨즈 맵과 섞을 수 있다.
const diffuseMaterial = new THREE.ShaderMaterial({ vertexShader: ` void main() { gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0); } `, fragmentShader: ` void main() { gl_FragColor = vec4(0.5,0.2,0.5,1.0); } ` });
먼저 디퓨즈 쉐이더이다. 단순히 오브젝트의 디퓨즈 정보를 내보내는 쉐이더이다.
const normalMaterial = new THREE.ShaderMaterial({ vertexShader: ` varying vec4 pos; void main() { pos = modelMatrix * vec4(normal,0.0); gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0); } `, fragmentShader: ` varying vec4 pos; void main() { vec3 nPos = vec3(pos); nPos = normalize(nPos); gl_FragColor = vec4((nPos+1.0)/2.0,1.0); } ` });
다음으로는 노말 쉐이더이다. 객체의 노말 정보를 내보낸다. 이 때 월드 행렬 정보만 곱해서 넘겨주는데 그 이유는 월드 공간에서 객체가 회전되는 경우 각 표면의 법선 벡터도 같이 회전되어야 하기 때문이다. 방향 벡터 정보이기 때문에 w 값은 0.0으로 지정하여 연산한다.
const geometryMaterial = new THREE.ShaderMaterial({ vertexShader: ` varying vec4 pos; void main() { pos = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0); } `, fragmentShader: ` varying vec4 pos; void main() { vec4 nPos = pos/pos.w; gl_FragColor = vec4((nPos+1.0)/2.0); } ` });
다음으로는 지오메트리 쉐이더이다. 각종 공간 좌표 정보를 텍스처에 담는 역할을 한다.
const postMaterial = new THREE.ShaderMaterial({ uniforms:{ inputTexture:{value: inputRenderTarget.texture}, normalGBuffer:{value:gBufferNormalTarget.texture}, geometryGBuffer:{value:gBufferGeometryTarget.texture}, diffuseGBuffer:{value:gBufferDiffuseTarget.texture}, lightPos:{value:new THREE.Vector3(0,0,0)} }, vertexShader: ` varying vec2 oUV; void main() { oUV = uv; gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D inputTexture; uniform sampler2D normalGBuffer; uniform sampler2D geometryGBuffer; uniform sampler2D diffuseGBuffer; uniform mat4 projectionMatrix; uniform mat4 modelMatrix; uniform vec3 lightPos; varying vec2 oUV; void main() { vec4 inputColor = texture(inputTexture, oUV); mat4 matrix = inverse(projectionMatrix * viewMatrix); vec4 geometryColor = texture(geometryGBuffer, oUV); vec4 geometry = matrix * vec4(vec3((geometryColor * 2.0) - 1.0), 1.0); geometry = geometry / geometry.w; vec4 normalColor = texture(normalGBuffer, oUV); vec4 normal = vec4(vec3((normalColor * 2.0) - 1.0),0.0); normal = vec4(normalize(vec3(normal)),0.0); vec4 diffuseColor = texture(diffuseGBuffer, oUV); vec3 lightToGeometry = normalize(vec3(geometry) - lightPos); float pointVelocity = dot(-lightToGeometry,vec3(normal)); vec4 finalColor = vec4(1.0) * pointVelocity ; finalColor.a=1.0; if(inputColor.a==0.0){ gl_FragColor=finalColor; }else{ gl_FragColor=clamp(finalColor + inputColor,0.0,1.0) ; } } ` });
마지막으로 각종 normalBuffer, geometryBuffer, diffuseBuffer Map을 읽어들여 입력한 맵과 합성하여 빛 연산후에 렌더링하는 쉐이더 코드이다. 특이점은 노말 맵을 읽여들여 해당 픽셀의 월드 공간 내에서의 노말 방향 벡터를, 그리고 지오메트리 맵을 읽여들여 Zbuffer 등을 함께 조합하여 원본 월드 좌표를 알아낸다. 이 후는 일반 난반사광 처리 방법과 동일하게 연산을 수행하고, 밝아지는 지점에 대해서 렌더링을 한다. 이 때 인풋 맵에 들어온 이전 연산 결과가 있는 경우 단순히 더해서 내보내고, 없으면 현재 연산된 빛 세기만 내보낸다. 여기서 주의할 점은 이 쉐이더는 빛의 세기만 내보내는 것이다. 디퓨즈 맵과 실제 합성하지는 않는다. 광원이 2개 있는 경우, 이 쉐이더를 2번 거쳐야하는데 그 과정에서 어두운 부분도 두 번 연산하면서 밝아야할 부분도 조금씩 더 어두워지는 문제가 존재한다. 따라서 빛 세기 정도만 내보내고 그 범위가 0,1 내를 유지하도록 구현한다.
const diffuseApplyMaterial = new THREE.ShaderMaterial({ uniforms:{ brightnessTexture:{value: null}, diffuseGBuffer:{value: gBufferDiffuseTarget.texture}, }, vertexShader: ` varying vec2 oUV; void main() { oUV = uv; gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D brightnessTexture; uniform sampler2D diffuseGBuffer; varying vec2 oUV; void main() { vec4 brightnessColor = texture(brightnessTexture, oUV); vec4 diffuseColor = texture(diffuseGBuffer, oUV); gl_FragColor = diffuseColor * brightnessColor; } ` });
그리고 마지막에 비로소 세기 맵과 디퓨즈 맵을 혼합하여 세기가 적용된 텍스처를 내보낸다.
파이프라인 과정
이 과정은 먼저 렌더링 할 오브젝트의 지오메트리, 디퓨즈, 노말 맵을 순차적으로 렌더링하고,
원하는 빛 위치에 대해서 빛 세기를 연산한다. 적어도 2개 이상을 연산하려는 경우 아웃으로 나온 맵을 다시 인풋으로 넣어 여러번 연산해주면 빛이 존재하는 부분만 밝은 세기 맵이 나온다.
그리고 마지막으로 세기 맵을 디퓨즈 맵에 합성하면 된다.
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.