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을 기준으로 하였다.) 정중앙으로 옮기는 새로운 좌표계 공간으로 이동할 수 있다.
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이상인 경우에만 해당 영역을 반사상 텍스처랑 같이 엮어서 렌더링한다. 그 외에는 오리지널 텍스처만 렌더링한다.