콘텐츠로 이동

Rust

alpha2 크레이트는 Rust 프로그램을 위한 엔진입니다. 이 페이지를 읽고 나면 GPU 디바이스를 열고, 프로젝트를 로드하고, 디코딩한 프레임을 엔진에 넘겨주고, 프리뷰 프레임을 렌더링하고, export를 프레임 단위로 진행하며 인코더에 넘길 YUV 플레인을 얻을 수 있습니다.

엔진은 wgpu를 통해 GPU에서 타임라인 프로젝트를 렌더링하고 오디오를 믹싱합니다. 미디어 파일을 읽거나 쓰지는 않습니다. 미디어 디코딩과 출력 인코딩은 여러분의 프로그램이 맡고, 엔진은 넘겨받은 프레임으로 프로젝트가 보여 주는 화면을 렌더링합니다.

Cargo.toml
[dependencies]
alpha2 = "0.5"
pollster = "1" # GPU 디바이스를 기다리는 데 쓸 실행기(어떤 것이든 가능)

이 크레이트는 엔진 크레이트들을 묶은 파사드입니다. 모듈은 곧 그 크레이트들로, audio, engine(재생과 export), gpu, media, model(프로젝트 문서), render, time이 있습니다. 대부분의 프로그램이 쓰는 타입은 최상위에도 있습니다. Project, EngineProject, GpuContext, Renderer, FrameProvider, SourceView, Player, Exporter, ExportStep, QualityProfile, RenderSize, MediaTime, FrameRate, TimeRange입니다.

프로젝트는 1280x720, 30fps로 이미지 하나를 1초 동안 보여 줍니다.

project.json
{
"schema_version": 1,
"id": "tutorial-project",
"name": "Tutorial project",
"settings": {
"width": 1280,
"height": 720,
"frame_rate": { "num": 30, "den": 1 },
"sample_rate": 48000
},
"sources": [
{ "id": "image-1", "locator": "asset://image-1", "kind": "image" }
],
"timeline": {
"tracks": [
{
"id": "video-1",
"name": "Main video",
"kind": "video",
"clips": [
{
"id": "clip-1",
"source_id": "image-1",
"timeline_range": { "start": 0, "end": 120000 },
"source_range": { "start": 0, "end": 120000 }
}
],
"transitions": []
}
]
}
}

프로그램은 프리뷰 프레임 하나를 렌더링한 다음 export 프레임을 모두 렌더링합니다.

src/main.rs
use alpha2::media::{SourceId, VideoCodec};
use alpha2::render::UploadTexture;
use alpha2::{
EngineProject, ExportStep, Exporter, FrameProvider, GpuContext, MediaTime, Player, Project,
QualityProfile, RenderSize, Renderer, SourceView,
};
/// 애플리케이션이 디코딩한 이미지. 소스 `image-1`로 렌더러에 넘겨줍니다.
struct Image {
id: SourceId,
pixels: UploadTexture,
}
impl FrameProvider for Image {
fn texture_for(&self, source: &SourceId, _time: MediaTime) -> Option<SourceView<'_>> {
(source == &self.id).then(|| SourceView::from(&self.pixels))
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let project = Project::from_json(&std::fs::read_to_string("project.json")?)?;
let project = EngineProject::new(project)?;
let gpu = pollster::block_on(GpuContext::request_product())?;
let mut renderer = Renderer::new(gpu);
// 프로젝트에서 이름으로 쓸 수 있는 내장 이펙트와 트랜지션.
renderer.register_builtins().expect("the built-in definitions register");
// 프로젝트 크기의 불투명한 중간 회색. 프리멀티플라이드 sRGB RGBA8입니다.
let size = RenderSize::new(1280, 720)?;
let gray = [128, 128, 128, 255].repeat(1280 * 720);
let image = Image {
id: SourceId::new("image-1")?,
pixels: renderer.upload_rgba8(size, &gray)?,
};
// 프리뷰 프레임 하나를 RGBA8로 읽어 옵니다.
let mut player = Player::new(QualityProfile::preview())?;
let frame = player.render_at(&project, &renderer, &image, MediaTime::ZERO)?;
if let Some(texture) = frame.texture {
let rgba = renderer.read_rgba8(texture)?;
println!("preview: {} bytes", rgba.len());
}
// export의 모든 프레임을 인코더에 넘길 YUV 4:2:0 플레인으로 얻습니다.
let mut exporter = Exporter::for_project(&project, QualityProfile::export(), VideoCodec::H264)?;
loop {
match exporter.render_next(&project, &renderer, &image)? {
ExportStep::Rendered { readback, .. } => {
let planes = readback.wait()?;
// planes.y, planes.u, planes.v: 인코더에 넘깁니다.
println!("frame: Y {} U {} V {} bytes", planes.y.len(), planes.u.len(), planes.v.len());
}
ExportStep::Waiting { missing_sources, .. } => {
return Err(format!("no frame for {missing_sources:?}").into());
}
ExportStep::Complete => break,
}
}
Ok(())
}
  1. 프로젝트를 로드합니다. Project::from_json이 문서를 읽고 검사합니다. EngineProject::new는 평가용 인덱스를 갖춘 불변 스냅샷을 만들며, 플레이어와 exporter는 이 스냅샷을 렌더링합니다. 편집할 때마다 새 스냅샷을 로드하세요.
  2. GPU를 엽니다. GpuContext::request_product()는 호스트가 렌더링에 쓰는 것과 같은 디바이스를 엽니다. 이 OS의 백엔드를 쓰고, ALPHA2_GPU_ADAPTER가 지정한 어댑터가 있으면 그것을, 없으면 내장 GPU보다 외장 GPU를 우선합니다.
  3. 렌더러를 만듭니다. Renderer::new(gpu)로 만든 뒤 register_builtins()로 내장 이펙트와 트랜지션을 등록합니다. 각각은 프레임이 처음 그것을 그리거나 warmup이 그것을 지정할 때 컴파일됩니다. 렌더러에 등록되지 않은 이펙트를 그리는 프레임은 실패합니다.
  4. 프레임을 넘겨줍니다. 디코딩한 미디어에 대해 FrameProvider를 구현합니다.
  5. 렌더링합니다. 프리뷰 프레임에는 Player, export 프레임에는 Exporter를 씁니다.

FrameProvider는 엔진이 요청한 정확한 소스 시간에 맞춰 각 소스의 프레임을 렌더러에 넘겨줍니다.

fn texture_for(&self, source: &SourceId, time: MediaTime) -> Option<SourceView<'_>>;

time은 소스 자체의 타임라인 위의 틱(tick) 시간이며, 트림하거나 재생 속도를 바꾼 클립에서는 타임라인 시간과 다릅니다. 아직 그 프레임이 없으면 None을 반환하세요. Renderer::upload_rgba8(size, pixels)는 프리멀티플라이드 sRGB RGBA8 픽셀을 UploadTexture로 바꾸며, 이것을 SourceView::from으로 넘겨줄 수 있습니다.

Player::new(QualityProfile::preview())로 플레이어를 만듭니다. render_at(&project, &renderer, &frames, time)은 주어진 시간에 보이는 프레임을 렌더링해 texture와 함께 반환합니다. renderer.read_rgba8(texture)로 이를 읽어 옵니다.

소스 프레임이 없으면 결과에 해당 소스가 표시되고(missing_sources), 플레이어의 누락 프레임 정책에 따라 마지막 프레임을 다시 보여 주거나(repeated) 아무것도 보여 주지 않습니다. 마지막 프레임과 같은 프레임은 다시 렌더링하지 않습니다(unchanged).

Exporter::for_project(&project, QualityProfile::export(), codec)은 export 품질로 타임라인 전체에 걸쳐 프로젝트의 프레임 그리드를 따라갑니다. codec(VideoCodec::H264 등)은 그 코덱이 기대하는 방식으로 크로마 샘플 위치를 정합니다. render_next를 호출할 때마다 ExportStep 하나가 반환됩니다.

단계 의미
Rendered { readback, .. } 프레임이 렌더링되었습니다. readback.wait()가 y, u, v 플레인을 돌려줍니다.
Waiting { missing_sources, .. } 이 소스들이 없어 프레임을 그리지 못했습니다. 소스를 공급하고 render_next를 다시 호출하면 같은 프레임부터 export가 이어집니다.
Complete 모든 프레임이 끝났습니다.

effective_range()는 요청한 범위를 프레임 그리드에 맞게 넓힌 범위이며, 그 시작점이 출력의 0초가 됩니다. audio_window(sample_rate)는 이에 대응하는 오디오 샘플 구간을 돌려줍니다.

request_product가 실패하면 GpuError::step()이 그 이유(GpuAcquisitionStep)를 알려 줍니다. 어댑터가 없음, 디바이스 요청 거부, 백엔드 시작 실패, ALPHA2_GPU_ADAPTER로 고정한 어댑터를 찾지 못함, 하드웨어 GPU 없음 중 하나입니다. 플랫폼이 알려 준 이유가 있으면 source()로 볼 수 있습니다.

엔진은 미디어 파일을 읽거나 쓰지 않습니다. alpha2-native는 FFmpeg 8로 디코딩, 프로브, 인코딩을 하며, 빌드하는 환경에 설치된 FFmpeg에 링크합니다. alpha2-host는 이를 사용해 프로젝트를 export하는 완전한 프로그램입니다.

alpha2::render::builtin_catalog()는 내장 이펙트와 트랜지션을 파라미터와 함께 나열합니다(이펙트 레퍼런스에도 있습니다). 크로마 키(alpha2.effect.chroma-key)와 3D LUT(alpha2.effect.lut-3d, .cube 파일을 Renderer::register_lut3d로 등록)도 내장 이펙트이며, 프로젝트에서 다른 이펙트와 똑같이 적용합니다. 이펙트와 컴포지션을 참고하세요.

기본 API에는 wgpu 타입이 드러나지 않습니다. 자체 wgpu 디바이스나 텍스처를 엔진과 공유하는 프로그램이라면 wgpu-interop 기능을 켜서 wgpu 타입을 쓰는 항목을 추가합니다.

  • GpuContext::from_parts는 여러분의 디바이스를 그대로 사용합니다.
  • SourceView::from_wgpu는 wgpu::TextureView를 소스로 넘겨줍니다.
  • 렌더링된 프레임의 wgpu 텍스처를 가져오거나, 하드웨어 인코더의 플레인에 직접 렌더링할 수 있습니다.
alpha2 = { version = "0.5", features = ["wgpu-interop"] }

기능을 켜면 alpha2::gpu::wgpu가 엔진이 빌드된 wgpu 크레이트를 가리킵니다. wgpu 타입은 직접 추가한 wgpu 의존성 대신 이 경로로 쓰세요.

  • 프로젝트 모델: project.json에 담기는 내용.
  • 결정론: 모든 런타임이 같은 프레임을 그리는 이유.
  • CLI: 디코딩과 인코딩이 내장된 엔진, alpha2-host.