첫 렌더링
This content is for v0.4. Switch to the latest version for up-to-date documentation.
이 튜토리얼에서는 프로젝트를 한 단계씩 직접 작성하고, 각 단계를 엔진이 이 페이지에서 렌더링하는 모습을 확인합니다. 마지막에는 완성한 프로젝트를 브라우저, Node.js, 명령줄, Rust, Python에서 실행합니다.
처음 네 단계를 따라 하는 데는 아무것도 설치할 필요가 없습니다. 이 페이지의 프리뷰는 모두 브라우저 안에서 엔진을 실행합니다.
1. 배경과 타이틀
섹션 제목: “1. 배경과 타이틀”프로젝트는 settings(캔버스 크기, 프레임 레이트, 오디오 샘플 레이트), sources, timeline으로 시작합니다. 여기서는 두 소스 모두 제너레이터(generated) 소스입니다. 엔진이 직접 그리므로 불러올 미디어가 없습니다. 하나는 그라디언트 채우기이고, 다른 하나는 텍스트 한 줄입니다.
각 소스는 트랙 위의 클립을 통해 타임라인에 나타납니다. 트랙은 뒤에서 앞 순서로 그려지므로 타이틀 트랙이 배경 트랙 뒤에 옵니다. 시간 단위는 틱이며 1초가 120,000틱입니다. 360000은 3초입니다.
{ "schema_version": 1, "id": "first-render", "name": "First render", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "backdrop", "kind": "generated", "generator": { "type": "fill", "paint": { "type": "linear_gradient", "start": { "x": 0, "y": 0 }, "end": { "x": 1, "y": 1 }, "stops": [ { "offset": 0, "color": { "red": 0.05, "green": 0.07, "blue": 0.2, "alpha": 1 } }, { "offset": 1, "color": { "red": 0.35, "green": 0.08, "blue": 0.4, "alpha": 1 } } ] } } }, { "id": "title", "kind": "generated", "generator": { "type": "text", "text": "My first render", "style": { "font_family": "Hanken Grotesk", "font_size": 0.1, "position": { "x": 0.5, "y": 0.9 }, "anchor": { "x": 0.5, "y": 1 } } } } ], "timeline": { "tracks": [ { "id": "background", "name": "Background", "kind": "video", "clips": [ { "id": "backdrop-clip", "source_id": "backdrop", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] }, { "id": "titles", "name": "Titles", "kind": "video", "clips": [ { "id": "title-clip", "source_id": "title", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] } ] }}timeline_range는 클립이 타임라인에서 차지하는 구간이고,source_range는 클립이 재생하는 소스의 구간입니다. 둘 다 반열린 구간[start, end)입니다.- 위치는 캔버스에 대한 비율이며,
(0, 0)이 왼쪽 위,(1, 1)이 오른쪽 아래입니다. 텍스트의anchor(여기서는 아래쪽 가운데)가position에 놓입니다. font_size는 캔버스 짧은 변에 대한 비율입니다.
2. 사진 추가
섹션 제목: “2. 사진 추가”이미지 소스를 하나 추가하고, 배경과 타이틀 사이에 이 소스를 위한 트랙을 넣습니다. locator는 엔진이 절대 열지 않는 불투명한 문자열이며, asset://coast가 무엇을 뜻하는지는 호스트가 정합니다. 이 페이지에서는 샘플 사진을 가리키고, 명령줄에서는 파일에 대응시킵니다.
클립의 visual이 사진을 80%로 줄이므로 주변으로 그라디언트가 보입니다. 생략한 필드는 기본값을 따릅니다. 기본값은 가운데 정렬, 회전 없음, 불투명입니다.
{ "schema_version": 1, "id": "first-render", "name": "First render", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "backdrop", "kind": "generated", "generator": { "type": "fill", "paint": { "type": "linear_gradient", "start": { "x": 0, "y": 0 }, "end": { "x": 1, "y": 1 }, "stops": [ { "offset": 0, "color": { "red": 0.05, "green": 0.07, "blue": 0.2, "alpha": 1 } }, { "offset": 1, "color": { "red": 0.35, "green": 0.08, "blue": 0.4, "alpha": 1 } } ] } } }, { "id": "title", "kind": "generated", "generator": { "type": "text", "text": "My first render", "style": { "font_family": "Hanken Grotesk", "font_size": 0.1, "position": { "x": 0.5, "y": 0.9 }, "anchor": { "x": 0.5, "y": 1 } } } }, { "id": "coast", "kind": "image", "locator": "asset://coast" } ], "timeline": { "tracks": [ { "id": "background", "name": "Background", "kind": "video", "clips": [ { "id": "backdrop-clip", "source_id": "backdrop", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] }, { "id": "photos", "name": "Photos", "kind": "video", "clips": [ { "id": "coast-clip", "source_id": "coast", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 }, "visual": { "scale": { "mode": "constant", "value": { "x": 0.8, "y": 0.8 } } } } ], "transitions": [] }, { "id": "titles", "name": "Titles", "kind": "video", "clips": [ { "id": "title-clip", "source_id": "title", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] } ] }}3. 컴포지션으로 사진 보정
섹션 제목: “3. 컴포지션으로 사진 보정”이펙트는 컴포지션, 즉 이미지 연산으로 이루어진 작은 그래프 안에 들어갑니다. 이 컴포지션은 클립의 그림("type": "clip")을 받아 alpha2.effect.color-controls로 채도를 높이고, alpha2.effect.vignette로 가장자리를 어둡게 합니다. 각 이펙트 노드는 input을 지정하고, output은 클립이 보여 줄 이미지를 내는 노드를 가리킵니다.
클립은 "composition": { "id": "look" }으로 이 컴포지션을 사용합니다. 이펙트 파라미터는 애니메이션할 수 있는 값이므로, 상수라도 { "mode": "constant", "value": ... } 형태로 쓰고 그 안에 타입이 있는 값을 넣습니다.
{ "schema_version": 1, "id": "first-render", "name": "First render", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "backdrop", "kind": "generated", "generator": { "type": "fill", "paint": { "type": "linear_gradient", "start": { "x": 0, "y": 0 }, "end": { "x": 1, "y": 1 }, "stops": [ { "offset": 0, "color": { "red": 0.05, "green": 0.07, "blue": 0.2, "alpha": 1 } }, { "offset": 1, "color": { "red": 0.35, "green": 0.08, "blue": 0.4, "alpha": 1 } } ] } } }, { "id": "title", "kind": "generated", "generator": { "type": "text", "text": "My first render", "style": { "font_family": "Hanken Grotesk", "font_size": 0.1, "position": { "x": 0.5, "y": 0.9 }, "anchor": { "x": 0.5, "y": 1 } } } }, { "id": "coast", "kind": "image", "locator": "asset://coast" } ], "compositions": [ { "id": "look", "nodes": [ { "id": "picture", "op": { "type": "clip" } }, { "id": "grade", "op": { "type": "effect", "input": "picture", "effect": { "id": "alpha2.effect.color-controls", "version": 1, "params": { "saturation": { "mode": "constant", "value": { "type": "float", "value": 1.3 } } } } } }, { "id": "vignette", "op": { "type": "effect", "input": "grade", "effect": { "id": "alpha2.effect.vignette", "version": 1, "params": { "amount": { "mode": "constant", "value": { "type": "float", "value": 0.6 } } } } } } ], "output": "vignette" } ], "timeline": { "tracks": [ { "id": "background", "name": "Background", "kind": "video", "clips": [ { "id": "backdrop-clip", "source_id": "backdrop", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] }, { "id": "photos", "name": "Photos", "kind": "video", "clips": [ { "id": "coast-clip", "source_id": "coast", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 }, "visual": { "scale": { "mode": "constant", "value": { "x": 0.8, "y": 0.8 } } }, "composition": { "id": "look" } } ], "transitions": [] }, { "id": "titles", "name": "Titles", "kind": "video", "clips": [ { "id": "title-clip", "source_id": "title", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] } ] }}4. 두 번째 사진과 트랜지션
섹션 제목: “4. 두 번째 사진과 트랜지션”같은 트랙에 사진을 하나 더 놓습니다. 한 트랙의 클립은 트랜지션이 이어 주는 구간에서만 겹칠 수 있으므로, 두 클립을 1초(2.5초~3.5초) 겹치게 하고 그 겹친 구간을 정확히 덮는 트랜지션을 넣습니다. 배경과 타이틀 클립은 6초로 늘립니다.
두 사진 클립은 모두 look 컴포지션을 다시 씁니다.
{ "schema_version": 1, "id": "first-render", "name": "First render", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "backdrop", "kind": "generated", "generator": { "type": "fill", "paint": { "type": "linear_gradient", "start": { "x": 0, "y": 0 }, "end": { "x": 1, "y": 1 }, "stops": [ { "offset": 0, "color": { "red": 0.05, "green": 0.07, "blue": 0.2, "alpha": 1 } }, { "offset": 1, "color": { "red": 0.35, "green": 0.08, "blue": 0.4, "alpha": 1 } } ] } } }, { "id": "title", "kind": "generated", "generator": { "type": "text", "text": "My first render", "style": { "font_family": "Hanken Grotesk", "font_size": 0.1, "position": { "x": 0.5, "y": 0.9 }, "anchor": { "x": 0.5, "y": 1 } } } }, { "id": "coast", "kind": "image", "locator": "asset://coast" }, { "id": "forest", "kind": "image", "locator": "asset://forest" } ], "compositions": [ { "id": "look", "nodes": [ { "id": "picture", "op": { "type": "clip" } }, { "id": "grade", "op": { "type": "effect", "input": "picture", "effect": { "id": "alpha2.effect.color-controls", "version": 1, "params": { "saturation": { "mode": "constant", "value": { "type": "float", "value": 1.3 } } } } } }, { "id": "vignette", "op": { "type": "effect", "input": "grade", "effect": { "id": "alpha2.effect.vignette", "version": 1, "params": { "amount": { "mode": "constant", "value": { "type": "float", "value": 0.6 } } } } } } ], "output": "vignette" } ], "timeline": { "tracks": [ { "id": "background", "name": "Background", "kind": "video", "clips": [ { "id": "backdrop-clip", "source_id": "backdrop", "timeline_range": { "start": 0, "end": 720000 }, "source_range": { "start": 0, "end": 720000 } } ], "transitions": [] }, { "id": "photos", "name": "Photos", "kind": "video", "clips": [ { "id": "coast-clip", "source_id": "coast", "timeline_range": { "start": 0, "end": 420000 }, "source_range": { "start": 0, "end": 420000 }, "visual": { "scale": { "mode": "constant", "value": { "x": 0.8, "y": 0.8 } } }, "composition": { "id": "look" } }, { "id": "forest-clip", "source_id": "forest", "timeline_range": { "start": 300000, "end": 720000 }, "source_range": { "start": 0, "end": 420000 }, "visual": { "scale": { "mode": "constant", "value": { "x": 0.8, "y": 0.8 } } }, "composition": { "id": "look" } } ], "transitions": [ { "from_clip": "coast-clip", "to_clip": "forest-clip", "timeline_range": { "start": 300000, "end": 420000 }, "easing": { "type": "linear" }, "effect": { "id": "alpha2.transition.fade", "version": 1 } } ] }, { "id": "titles", "name": "Titles", "kind": "video", "clips": [ { "id": "title-clip", "source_id": "title", "timeline_range": { "start": 0, "end": 720000 }, "source_range": { "start": 0, "end": 720000 } } ], "transitions": [] } ] }}이것으로 프로젝트가 완성되었습니다. first-render.json으로 저장하세요.
실행하기
섹션 제목: “실행하기”모든 대상 환경이 같은 파일을 읽습니다. 달라지는 것은 미디어와 폰트를 엔진에 전달하는 방법입니다. 프로젝트는 coast와 forest라는 소스를 사용하며, 각 호스트가 이 id를 실제 그림에 대응시킵니다.
npm install alpha2. 앱이 사진을 디코딩해서 픽셀을 업로드하고, 엔진은 캔버스에 그립니다.
import init, { WebEffectRegistry } from "alpha2/wasm";
await init();const engine = await WebEffectRegistry.createForHtmlCanvas(document.querySelector("canvas"));
// 텍스트는 등록된 폰트로 그립니다: 프로젝트가 요청하는 패밀리 이름, 폰트 바이트, 페이스 인덱스.const font = new Uint8Array(await (await fetch("/fonts/HankenGrotesk.ttf")).arrayBuffer());engine.addFont("Hanken Grotesk", font, 0);
const project = await (await fetch("/first-render.json")).json();engine.loadProject(JSON.stringify(project));
// premultiplied sRGB RGBA8. 이 사진들은 불투명하므로 캔버스 픽셀이 이미 premultiplied입니다.async function rgbaOf(url, width, height) { const bitmap = await createImageBitmap(await (await fetch(url)).blob()); const canvas = new OffscreenCanvas(width, height); const context = canvas.getContext("2d"); context.drawImage(bitmap, 0, 0, width, height); return new Uint8Array(context.getImageData(0, 0, width, height).data.buffer);}engine.uploadRgbaImage("coast", 1280, 720, await rgbaOf("/coast.jpg", 1280, 720));engine.uploadRgbaImage("forest", 1280, 720, await rgbaOf("/forest.jpg", 1280, 720));
engine.renderAt(360_000n); // 3초 지점, 트랜지션 한가운데의 프레임renderAt은 프레임이 없었던 소스를 알려 주는 JSON을 반환합니다. 파일로 export하는 방법은 브라우저를 참고하세요. beginExport가 프로젝트의 프레임 그리드를 따라가며 인코더에 넘길 I420 프레임을 돌려줍니다.
npm install @alpha2-sh/node를 실행한 뒤 npx @alpha2-sh/node install로 alpha2-host를 받습니다. 사진 디코딩과 MP4 인코딩은 호스트가 직접 합니다.
import { readFile } from "node:fs/promises";import { exportProject } from "@alpha2-sh/node";
const project = JSON.parse(await readFile("first-render.json", "utf8"));await exportProject({ project, assets: { coast: "coast.jpg", forest: "forest.jpg" }, // 소스 id별 파일 fonts: ["HankenGrotesk.ttf"], startTicks: 0, endTicks: 720_000, // 6초 frameRate: { num: 30, den: 1 }, output: "first-render.mp4",});진행 상황, 취소, 스틸, 오류 처리는 Node.js를 참고하세요.
릴리스 아카이브에서 alpha2-host를 받습니다(CLI 참고). --asset마다 소스 하나의 파일을, --font로 폰트 파일을 지정합니다.
alpha2-host export --project first-render.json \ --asset coast=coast.jpg --asset forest=forest.jpg \ --font HankenGrotesk.ttf \ --out first-render.mp4기본적으로 프로젝트의 프레임 레이트로 타임라인 전체를 export합니다. 결과는 표준 출력에 JSON으로 출력됩니다.
cargo add alpha2. 프로그램이 문서를 읽고 검사한 다음, 엔진에 빌려준 프레임으로 렌더링합니다.
use alpha2::{EngineProject, Project};
let project = Project::from_json(&std::fs::read_to_string("first-render.json")?)?;let project = EngineProject::new(project)?;// 디코딩한 `coast`와 `forest` 그림을 `FrameProvider`로 엔진에 빌려준 다음,// `Player::render_at`으로 렌더링하거나 `Exporter::render_next`로 export합니다.엔진 크레이트는 파일을 디코딩하거나 인코딩하지 않습니다. GPU 컨텍스트, FrameProvider, 프리뷰 프레임, export 루프까지 갖춘 전체 프로그램은 Rust에 있습니다.
pip install alpha2. 지원하는 플랫폼에서는 alpha2-host도 함께 설치됩니다. 프로젝트는 평범한 dict입니다.
import json
import alpha2
with open("first-render.json") as file: project = json.load(file)
print(alpha2.validate_project(project)) # 엔진이 받아들이면 []result = alpha2.export( project, "first-render.mp4", assets={"coast": "coast.jpg", "forest": "forest.jpg"}, fonts=["HankenGrotesk.ttf"],)print(result["output"], result["frames"])스틸, 작성 도우미, compose API는 Python을 참고하세요.
