on
Let's Build a Browser Engine!
제7 장: 그리기 입문
이 글은 Matt Brubeck의 Let’s Build a Browser Engine!을 번역한 글입니다.
알림 : 부족한 실력 탓에 잘못된 번역, 부자연스러운 문장이 있을 수 있습니다. 해당 문제에 대한 의견을 댓글이나 GitHub 저장소 Pull Request를 통해 제안해 주시면 감사한 마음으로 적극 반영하도록 하겠습니다. 감사합니다.
Let’s Build a Browser Engine!
제7 장: 그리기 입문
토이 브라우저 엔진 구축 시리즈의 마지막 장으로 돌와왔습니다.
이번 장에서는 매우 기초적인 그리기 코드를 추가합니다. 이 코드는 레이아웃 모듈로부터 박스의 트리를 입력받아 픽셀 배열로 변환합니다. 이 처리 과정을 “래스터화(rasteriazation)” 라고 합니다.
대부분의 브라우저들은 그래픽 API와 Skia, Cairo, Direct2D와 같은 라이브러리의 도움을 받아 래스터화를 구현합니다. 이러한 API들은 다각형(Polygon), 선(Line), 곡선(Curve), 그레이디언트(Gradient), 텍스트(Text)를 그리는 기능을 제공합니다. 당분간은 사각형만을 그릴 수 있는 필자의 래스터라이저를 사용해보겠습니다.
필자는 궁극적으로 텍스트 렌더링을 구현하고 싶습니다. 따라서 현시점에서 토이 그리기 코드를 버리고 “실제” 2D 그래픽 라이브러리로 바꿀 수도 있을 것입니다. 그러나 현재로서는 블록 레이아웃 알고리즘의 출력을 그림으로 변환하기엔 사각형만으로도 충분합니다.
변경 사항
지난번 포스트 이후로, 이전 장의 코드를 약간 수정했습니다. 여기에는 몇 가지 리팩토링과 최신 Rust 빌드에 호환되도록 하기 위한 업데이트가 포함되어 있습니다. 변경 사항이 코드를 이해하는데 중요한 것은 아니므로 독자가 궁금하다면 커밋 기록을 확인해보길 바랍니다.
디스플레이 리스트 만들기
그리기 작업을 수행하기 전에 레이아웃 트리를 탐색하여 디스플레이 리스트를 만들어야 합니다. 이것은 “원 그리기” 또는 “텍스트의 문자열 그리기”와 같은 그래픽 명령의 리스트입니다. 우리의 경우는 단지 “사각형 그리기” 입니다.
그런데 왜 명령을 직접 실행하지 않고 디스플레이 리스트에 집어넣을까요? 디스플레이 리스트는 몇 가지 이유로 유용합니다. 먼저 이후 명령들에 의해 완전히 가려지는 명령 항목을 찾을 수 있고 이를 제거하여 불필요한 그리기 작업을 제거할 수 있습니다. 또한 특정 항목만이 변경된 경우 디스플레이 리스트를 약간 수정하여 다시 사용할 수 있습니다. 그리고 하나의 디스플레이 리스트를 사용하여 다른 유형의 출력을 생성할 수 있습니다. 예를 들어 하나의 리스트로 화면에 표시할 픽셀을 만들거나 프린터로 보낼 벡터 그래픽을 만들 수 있습니다.
Robinson의 디스플레이 리스트는 DisplayCommands
의 벡터입니다. 현재 DisplayCommands
에는 단색(solid-color) 사각형 유형만 존재합니다.
type DisplayList = Vec<DisplayCommand>;
enum DisplayCommand {
SolidColor(Color, Rect),
// 여기에 명령을 추가하세요.
}
디스플레이 리스트를 만들기 위해서는 레이아웃 트리를 탐색하고 각 박스에 대한 일련의 명령을 생성해야 합니다. 먼저 상자의 배경을 그리고 배경 위에 테두리와 내용을 그립니다.
fn build_display_list(layout_root: &LayoutBox) -> DisplayList {
let mut list = Vec::new();
render_layout_box(&mut list, layout_root);
return list;
}
fn render_layout_box(list: &mut DisplayList, layout_box: &LayoutBox) {
render_background(list, layout_box);
render_borders(list, layout_box);
// TODO: 텍스트 렌더링
for child in &layout_box.children {
render_layout_box(list, child);
}
}
기본적으로 HTML 요소는 순서대로 쌓입니다. 두 요소가 겹치는 경우 이전 요소 위에 이후 요소가 그려집니다. 이 특징은 DOM 트리에 나타나는 것과 동일한 순서로 요소를 그리게 되는 디스플레이 리스트에도 반영됩니다. 만약 z-index 속성을 지원한다면 개별 요소가 쌓이는 순서를 재정의 할 수 있으며 그에 따라 디스플레이 리스트도 정렬해야 할 것입니다.
배경은 쉽습니다. 배경은 그저 빈틈없이 채워진 직사각형입니다. 배경색이 지정되지 않았다면 배경이 투명하므로 디스플레이 명령을 생성할 필요가 없습니다.
fn render_background(list: &mut DisplayList, layout_box: &LayoutBox) {
get_color(layout_box, "background").map(|color|
list.push(DisplayCommand::SolidColor(color, layout_box.dimensions.border_box())));
}
// CSS 속성 `name`에 지정된 색상, 없는 경우 None을 반환합니다.
fn get_color(layout_box: &LayoutBox, name: &str) -> Option<Color> {
match layout_box.box_type {
BlockNode(style) | InlineNode(style) => match style.value(name) {
Some(Value::ColorValue(color)) => Some(color),
_ => None
},
AnonymousBlock => None
}
}
테두리도 이와 유사합니다. 대신 사각형 하나가 아닌 각 모서리에 해당하는 4개의 사각형을 그립니다.
fn render_borders(list: &mut DisplayList, layout_box: &LayoutBox) {
let color = match get_color(layout_box, "border-color") {
Some(color) => color,
_ => return // bail out if no border-color is specified
};
let d = &layout_box.dimensions;
let border_box = d.border_box();
// 왼쪽 테두리
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x,
y: border_box.y,
width: d.border.left,
height: border_box.height,
}));
// 오른쪽 테두리
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x + border_box.width - d.border.right,
y: border_box.y,
width: d.border.right,
height: border_box.height,
}));
// 위쪽 테두리
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x,
y: border_box.y,
width: border_box.width,
height: d.border.top,
}));
// 아래쪽 테두리
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x,
y: border_box.y + border_box.height - d.border.bottom,
width: border_box.width,
height: d.border.bottom,
}));
}
이후 렌더링 함수는 전체 레이아웃 트리가 디스플레이 명령으로 변환 완료될 때까지 각 상자의 자식 상자들을 그려 나갑니다.
래스터화
이제 디스플레이 리스트를 만들었으니 각 DisplayCommand
를 실행하여 픽셀로 변환해야 합니다. 픽셀은 Canvas
에 저장됩니다.
struct Canvas {
pixels: Vec<Color>,
width: usize,
height: usize,
}
impl Canvas {
// 빈 캔버스를 만듭니다.
fn new(width: usize, height: usize) -> Canvas {
let white = Color { r: 255, g: 255, b: 255, a: 255 };
return Canvas {
pixels: repeat(white).take(width * height).collect(),
width: width,
height: height,
}
}
// ...
}
캔버스에 직사각형을 그리기 위해선 캔버스의 경계 밖으로 나가지 않도록 하는 헬퍼 메소드를 사용하여 행과 열을 반복해서 탐색하면 됩니다.
fn paint_item(&mut self, item: &DisplayCommand) {
match item {
&DisplayCommand::SolidColor(color, rect) => {
// 캔버스 경계에 맞춰 직사각형을 자릅니다.
let x0 = rect.x.clamp(0.0, self.width as f32) as usize;
let y0 = rect.y.clamp(0.0, self.height as f32) as usize;
let x1 = (rect.x + rect.width).clamp(0.0, self.width as f32) as usize;
let y1 = (rect.y + rect.height).clamp(0.0, self.height as f32) as usize;
for y in (y0 .. y1) {
for x in (x0 .. x1) {
// TODO: 기존 픽셀과 알파 합성
self.pixels[x + y * self.width] = color;
}
}
}
}
}
이 코드는 불투명 색상의 경우에만 작동합니다. 만약 투명도를 추가하고자 한다면 (opacity
속성을 읽거나 CSS 파서에 rgba()
값에 대한 지원을 추가하여) 각각의 픽셀을 그 위에 그려지는 무언가와 혼합 해야 할 것입니다.
이제 모든 것을 paint
함수에 통합하겠습니다. 이 함수는 디스플레이 리스트를 만든 다음 캔버스에 래스터화 합니다.
// LayoutBox 트리를 픽셀의 배열로 그립니다.
fn paint(layout_root: &LayoutBox, bounds: Rect) -> Canvas {
let display_list = build_display_list(layout_root);
let mut canvas = Canvas::new(bounds.width as usize, bounds.height as usize);
for item in display_list {
canvas.paint_item(&item);
}
return canvas;
}
마지막으로 Rust 이미지 라이브러리를 사용하기 위한 몇 줄의 코드를 추가하여 픽셀 배열을 PNG 파일로 저장합니다.
아름다운 결과
마침내 렌더링 파이프라인의 끝에 도착하였습니다. 1000줄 미만의 코드인 Robinson은 HTML 파일을 파싱 할 수 있게 되었습니다.
<div class="a">
<div class="b">
<div class="c">
<div class="d">
<div class="e">
<div class="f">
<div class="g">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
… 그리고 CSS 파일은 다음과 같으며
* { display: block; padding: 12px; }
.a { background: #ff0000; }
.b { background: #ffa500; }
.c { background: #ffff00; }
.d { background: #008000; }
.e { background: #0000ff; }
.f { background: #4b0082; }
.g { background: #800080; }
… 이렇게 생성합니다.
![]() |
오예!
연습
-
래스터 이미지 대신 벡터 출력(예를 들어, SVG 파일)을 생성하는 그리기 함수를 작성해보세요.
-
opacity와 알파 합성 지원을 추가해보세요.
-
캔버스 경계를 완전히 벗어난 항목을 도태시켜 디스플레이 리스트를 최적화하는 기능을 작성해보세요.
-
독자가 OpenGL에 익숙하다면 GL shaders를 사용하여 직사각형을 그리는 하드웨어 가속 그리기 함수를 작성해보세요.
다음에 계속…
이젠 렌더링 파이프라인 각 단계에 대한 기본 기능을 갖추었으니 되돌아가 빠진 기능들을 채워 넣을 때입니다. 특히, 인라인 레이아웃과 텍스트 렌더링을 말입니다. 추가로 네트워킹과 스크립팅과 같은 추가 단계도 추가할 수 있을 것입니다.
필자는 이번 달에 Bay Area Rust Meetup에서 “Let’s build a browser engine!”의 짧은 세미나를 진행합니다. 이 세미나는 내일 오후 7시 (11월 6일, 목요일) Mozilla 샌프란시스코 지사에서 열리며, 필자의 Servo 개발 동료들의 Servo 강연도 함께 진행될 예정입니다. 세미나는 Air Mozilla에서 생중계되며, 녹화본도 공개될 예정입니다.
Discussion and feedback