GLSL 및 OpenGL(WebGL) 에서 평면 반사 행렬 통한 반사 구현 빛 반사 - Whitmem
GLSL 및 OpenGL(WebGL) 에서 평면 반사 행렬 통한 반사 구현 빛 반사
Graphic Development
2025-01-12 22:22 게시 3f3ea341fce5ade1b1a5

0
0
62
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
평면 반사 행렬을 통한 빛 반사의 구현
오브젝트를 그릴 때 평면에 반사된 지오메트리 위치에서 한 번더 그리고 똑같은 방법으로 쉐이딩을 수행한다. 그리고 거울 표면에만 그릴 수 있도록 스텐실 버퍼나 마스킹 텍스처를 사용해 거울 위에만 렌더링한다.
이 방법은 비교적 제일 간단한 방법으로 반사가 조금 부정확하고 어색할 수 있지만 눈속임으로 나쁘지 않은 방법으로 보인다.
이 방법은 기본적으로 거울 평면 위에 존재하는 객체들을 그대로 뒤집어서 아래에도 출력하는 것이 기본 원리이다.
즉 평면으로부터 뒤집어 한번 더 똑같이 렌더링 해주면 된다.
그런데 중요한 것은 반사상은 거울에 평면 위에 직접적으로 그려지는 듯하게 보여야하므로, 거울 안에만 반사상이 맺히게끔 해야 한다. 반사된 객체가 거울을 뚫고 아래에 그려지더라도 각도나 바라보는 모습에 따라서 거울에 존재하지 않는 반사 객체들은 표시할 필요가 없다.
위 예시에서, 거울 저 너머로 반사된 객체들의 분홍색 영역은 거울 바깥에 존재한다. 이 부분은 잘라서 렌더링해야 조금 자연스러운 반사 렌더링이 가능하다.
이런식으로 말이다. 이 작업을 렌더링 상에서 수행하기 위해서는 마스킹 개념을 사용하면 된다. 대표적으로 스텐실 버퍼를 사용할 수 있다.
거울이 존재하는 영역을 검정색 텍스처로 렌더링하고 이 검정색 텍스처인 공간에만 추가적으로 포스트 프로세싱에서 배경들을 렌더링해주면 된다.
원본 오브젝트를 렌더링할 때는 빛 처리가된 원본 오브젝트만 렌더링되고 거울 표면은 흰색으로만 렌더링한다. 어차피 거울 표면은 후 부분에서 해당 영역을 마스킹 교차 검사를 통해 부분적으로 반대에 그려지는 상의 객체들의 색상을 적용해줄 것이기 때문이다.
그리고, 스텐실 텍스처는 거울이 존재하는 부분만 밝게 색칠한 텍스처이다. 검은 영역에서는 반대 방향에 존재하는 상을 그릴 필요가 없는 것이다.
물 밑에 렌더링되는 반사된 상 정보이다. 흔들거리는 효과는 따로 포스트 프로세싱을 넣어줘서 그렇다. 사실은 물에 반사된 벡터로 또 다시 그렸을뿐이다.
const renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, }); const mirrorStenecilTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, }); const originalTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, }); const reflectedTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, });
즉 각각의 영역을 담고 보관 처리할 텍스처가 존재해야하고, 단계별로 렌더링해서 위 타겟들에게 차곡 차곡 쌓아 그려줘야 한다.
노말, 탄젠트, 비트 탄젠트
for (let i = 0; i < mirrorGeometry.attributes.tangent.count; i++) { const normal = new THREE.Vector3().fromBufferAttribute(mirrorGeometry.attributes.normal, i); const tangent = new THREE.Vector3().fromBufferAttribute(mirrorGeometry.attributes.tangent, i); const bitangent = new THREE.Vector3().crossVectors(normal, tangent).normalize(); bitangents.setXYZ(i, bitangent.x, bitangent.y, bitangent.z); } mirrorGeometry.setAttribute('bitangent', bitangents);
기본적으로 거울 표면을 기준으로 반사해야하기 때문에 거울의 법선 벡터와, 탄젠트 벡터, 비트 탄젠트 벡터를 구하기로 하였다. 이 세가지 정보는 서로 직교하고 기저벡터로 사용하면, 거울 표면 어느 위치를(필자는 로컬 좌표계의 기본인 거울 표면의 정 중앙 origin을 기준으로 하였다.) 정중앙으로 옮기는 새로운 좌표계 공간으로 이동할 수 있다.
즉 월드 좌표계에 존재하는 특정 객체와 거울들을 1)거울 표면의 어느 한 점이 중앙에 가도록 행렬을 만들어 이동하고, 2) y나 좌표계를 보고 뒤집고, 3) 역행렬을 통해 복귀할 수 있다. 굳이 위와 같은 과정을 거치는 이유는 거울 표면과 객체가 어떤 관계로 존재하든간에 거울을 정모습을 바라보게끔 이동시키면 y나 좌표계 축으로만 뒤집어주면 되기 때문에 연산이 매우 직관적이다.
즉 아무리 복잡한 회전 관계를 가지고 있던 간에, 중앙 좌표계로 전체 이동시키고 회전 역시 거울의 모습이 정상화되는 방향으로 행렬을 만들어준 뒤, 모든 객체에 적용시키고, 각 객체를 원하는 방향 축으로만 단순 - 연산을 통해 뒤집어주고, 역행렬을 통해 복귀 시켜주면 그 작업이 완료된다. 특히 행렬로 만들어두면 단순히 특정 좌표에 행렬을 곱하면 반사된 위치가 바로 나오게끔 행렬을 구성할 수 있다.
노말, 비트 탄젠트, 탄젠트, 거울 객체의 원본 행렬 가져와 연산
let mNormal = new THREE.Vector3().fromBufferAttribute(mirrorGeometry.attributes.normal, 0); let mBitangent = new THREE.Vector3().fromBufferAttribute(mirrorGeometry.attributes.bitangent, 0); let mTangent = new THREE.Vector3().fromBufferAttribute(mirrorGeometry.attributes.tangent, 0); let mirrorMatrix = mirror.matrixWorld; let worldNormal = new THREE.Vector4(mNormal.x,mNormal.y,mNormal.z, 0).applyMatrix4(mirrorMatrix); let worldBitangent = new THREE.Vector4(mBitangent.x,mBitangent.y,mBitangent.z, 0).applyMatrix4(mirrorMatrix); let worldTangent = new THREE.Vector4(mTangent.x,mTangent.y,mTangent.z, 0).applyMatrix4(mirrorMatrix); let posZero = new THREE.Vector4(0.0,0.0,0.0,1.0); let posTranslated = posZero.applyMatrix4(mirrorMatrix);
먼저 모든 객체들은 거울 객체의 법선이 정상 모습을 바라보게끔 회전되어야 하고, 거울 중심 좌표가 중앙에 가도록 이동해야하므로 거울의 월드 행렬을 가져온다. 기존 객체의 노말 벡터 등은 단순히 객체의 로컬 좌표를 기준으로 법선을 바라보기 때문에, 법선 벡터 역시 월드 행렬에 맞게끔 회전해줘야 한다. 거울이 회전된 경우 법선도 같이 회전되기 때문이다. 특히 법선은 방향만 나타내기 때문에, 각각 연산시에 w 값을 0으로 해주어 이동을 제외하고 방향으로만 계산되도록 한다. 그리고 posZero 는 정점을 의미하는데 월드 좌표를 기준으로 0,0,0에 있을 때 거울이 이동한 행렬을 적용하면 이 정점이 어디로 이동하는지 확인하기 위함이다. 거울 표면을 정 중앙으로 옮겨주기 위해서 월드 행렬에서 이동한 만큼 다시 역으로 되돌리기 위해 그 x,y,z 거리를 다시 담아둔다.
거울 이동 행렬 구현
const translateMatrix = new THREE.Matrix4(); translateMatrix.set( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -posTranslated.x, -posTranslated.y, -posTranslated.z, 1.0 ); translateMatrix.transpose();
먼저 역 이동행렬을 만들어준다. 위 이동 행렬을 적용하면 거울 표면이 이동한 거리만큼 역으로 다시 중앙으로 가게끔 이동한다.
거울 회전 행렬 구현
let rotation = new THREE.Matrix4(); rotation.set( worldBitangent.x, worldBitangent.y, worldBitangent.z, 0.0 , worldNormal.x, worldNormal.y, worldNormal.z, 0.0, worldTangent.x, worldTangent.y, worldTangent.z, 0.0, 0.0, 0.0, 0.0, 1.0 ); rotation.transpose();
그런다음 각 직교 관계를 가지는 노말, 탄젠트, 비트탄젠트 기저벡터가 존재할 때 원본 방향으로 되돌리기 위한 행렬을 만든다. 즉 left, up, forward 벡터를 넣어주는데 bitangent를 left, normal을 up, tangent를 forward 벡터로 보고 원본으로 회전하는 행렬을 만든다.
반사 행렬 생성
let yFlip = new THREE.Matrix4(); yFlip.set( 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ); yFlip.transpose();
마지막으로 y를 뒤집는 행렬을 만든다. 위 회전까지만 했을 때 y를 뒤집으면 거울의 반대편에 그려지는 것을 눈으로 확인하고 행렬을 만든 것이다. 만약 작업 과정에서 좌표계가 뒤집어졌다면 뒤집어진 방향을 기준으로 거울 반대에 그려지도록 뒤집어지는 행렬을 만들어주면 된다. 어차피 다시 원본으로 되돌려주는 역 행렬을 곱할 것이기 때문에 큰 상관은 없다.
최종 연산 행렬 취합
rotation = rotation.transpose(); let mirrorZeroMatrix = rotation.clone(); mirrorZeroMatrix.multiply(translateMatrix) ; let calculatedWorldMatrix = mirrorZeroMatrix.clone(); calculatedWorldMatrix.invert(); calculatedWorldMatrix.multiply(yFlip).multiply(mirrorZeroMatrix);
거울을 원점으로 이동하는 행렬을 만들어준다. 이 행렬의 역행렬도 계산하여 원점으로 이동하여 뒤집고 다시 원본으로 이동하는 최종 행렬을 만든다.
렌더링
이제 순차적으로 오리지널 객체를 렌더링하고, 반사된 오브젝트를 렌더링하고 거울 영역의 스텐실 버퍼를 렌더링하고, 스텐실 버퍼에 반사된 오브젝트를 병합해주면 된다. 이 과정에서 새로운 쉐이더로 넘겨주면 기존 객체의 반사 효과나 기존 쉐이더를 사용하지 못하기 때문에 기존 객체의 worldMatrix 에 방금 계산한 최종 연산 행렬 * 월드 매트릭스를 곱해서 넘겨주었다.
for(let i =0;i<reflectedRender.length;i++){ let objectItem = reflectedRender[i]; objectItem.matrixAutoUpdate = true; objectItem.updateMatrixWorld(true); let originalMatrix = objectItem.matrixWorld.clone(); let reflectionWorldMatrix = calculatedWorldMatrix.clone(); reflectionWorldMatrix.multiply(originalMatrix); objectItem.matrix.copy(reflectionWorldMatrix); objectItem.matrixAutoUpdate = false; objectItem.updateMatrixWorld(true); objectItem.originalMatrix =originalMatrix; } ... 중략
즉 쉐이더안에서 계산해줘도 되는데, 이러면 새로운 쉐이더를 만들면서 기존 쉐이더를 사용할 수 없게 되는 문제가 있었다. 따라서 객체에는 빛 연산 등이 수행되는 쉐이더를 넣어두고 각 쉐이더의 matrixWorld 를 임의로 거울 원점 * matrixWorld 행렬로 넣어 계산하였다. 이 때 거울 원점을 먼저 곱해야하는데, 결과적으로 matrixWorld * localSpace 먼저 계산된 뒤에 그 다음에 비로소 거울 원점으로 이동하는 작업이 수행되어야 하기 때문이다. 즉 월드 공간에서 원점으로 이동하여 뒤집는 행렬을 구성했기 때문에 월드 공간으로 먼저 옮긴 뒤에 거울 원점 반사 행렬을 적용해야 한다. 따라서 행렬식에는 먼저 거울 원점 반사 행렬을 곱하고 matrixWorld 을 곱하면, 실제로는 오브젝트의 로컬 좌표계에서 먼저 첫 번째로 matrixWorld 를 연산하고, 다음으로 거울 반사 행렬을 연산하게 된다.
void main() { vec2 uvPosition = oUV; vec4 originalTargetColor = texture(originalTarget, uvPosition); vec4 mirrorStencilTargetColor = texture(mirrorStencilTarget, uvPosition); vec2 reflectedUVMix = uvPosition + sin(uvPosition.xy*100.0 +uTime*10.0)*0.002; vec4 reflectedObjectTargetColor = texture(reflectedObjectTarget, reflectedUVMix); vec4 combinedColor = originalTargetColor; if(mirrorStencilTargetColor.r>=1.0){ combinedColor = vec4(0.6,0.6,0.8,1.0); combinedColor=(combinedColor*0.3 + reflectedObjectTargetColor*0.9); combinedColor.a=1.0; } gl_FragColor = vec4(combinedColor); }
픽셀 쉐이더 일부를 나타낸 것이다. 원본이 그려진 텍스처를 가져오고, 스텐실 버퍼가 그려진 텍스처를 가져오고 해당 부분에 반사상의 텍스처를 가져온다. 이 때 약간 울렁이는 효과를 주기 위해서 uv 정보를 sin과 엮어서 굴절 효과를 준다. 이와 동시에 스텐실 버퍼가 1.0이상인 경우에만 해당 영역을 반사상 텍스처랑 같이 엮어서 렌더링한다. 그 외에는 오리지널 텍스처만 렌더링한다.
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.