clipping 구현: scissor, stencil, mask texture
frame clipping은 editor 모델에서는 하나의 속성처럼 보이지만, renderer에서는 여러 구현 방식으로 나뉩니다.
clip strategy를 고르는 코드
clip 형태에 따라 가장 싼 renderer 전략을 선택합니다.
function chooseClipStrategy(frame) {
if (!frame.clipsContent) return "none";
if (!frame.rotation && frame.cornerRadius === 0) {
return "scissor";
}
if (frame.clipPath && frame.clipPath.isSimple) {
return "stencil";
}
return "mask-texture";
}
function applyScissor(gl, screenRect, dpr) {
gl.enable(gl.SCISSOR_TEST);
gl.scissor(
Math.round(screenRect.x * dpr),
Math.round(screenRect.y * dpr),
Math.round(screenRect.width * dpr),
Math.round(screenRect.height * dpr)
);
}
scissor는 WebGL viewport처럼 좌하단 기준입니다. DOM/canvas screen rect가 좌상단 기준이면 y를 뒤집어야 합니다.
function domRectToScissor(rect, viewportHeight, dpr) {
return {
x: Math.round(rect.x * dpr),
y: Math.round((viewportHeight - rect.y - rect.height) * dpr),
width: Math.round(rect.width * dpr),
height: Math.round(rect.height * dpr)
};
}
function applyFrameScissor(gl, frameScreenRect, viewportHeight, dpr) {
const box = domRectToScissor(frameScreenRect, viewportHeight, dpr);
gl.enable(gl.SCISSOR_TEST);
gl.scissor(box.x, box.y, box.width, box.height);
}
clip stack이 겹칠 때는 부모와 자식 scissor의 intersection을 사용합니다.
function intersectRect(a, b) {
const x1 = Math.max(a.x, b.x);
const y1 = Math.max(a.y, b.y);
const x2 = Math.min(a.x + a.width, b.x + b.width);
const y2 = Math.min(a.y + a.height, b.y + b.height);
return { x: x1, y: y1, width: Math.max(0, x2 - x1), height: Math.max(0, y2 - y1) };
}
비용 순서로 생각한다
axis-aligned rectangle -> scissor
shape mask -> stencil
image/filter mask -> mask texture / render target
가장 단순한 clip은 scissor입니다. 하지만 회전된 frame이나 arbitrary path mask는 추가 pass나 stencil이 필요할 수 있습니다.
오늘의 핵심
clip은 보이는 영역을 줄이는 기능이지만, renderer에서는 state와 pass를 늘리는 비용이 될 수 있습니다.