frontend API
包 Luna-Flow/geometry3d/frontend 把场景和相机转换为与后端无关的、已着色并已投影的三角形列表(DrawList)。它还拥有 Canvas 后端和曝光效果使用的标量图像软件深度缓冲(LumaBuffer)、块匹配光流估计器、曝光设置和动画时间轴。它对字符、颜色和 DOM 一无所知。
import {
"Luna-Flow/geometry3d/core",
"Luna-Flow/geometry3d/view",
"Luna-Flow/geometry3d/frontend",
"Luna-Flow/linear-algebra/mutable" @la,
}
frontend 设计解释了管线、阴影贴图和光栅化规则;frontend 教程展示了它们的用法。
场景
SceneObject
SceneObject 是一个网格及其模型变换。
pub struct SceneObject {
mesh : @core.Mesh
transform : @core.Transform3
}
SceneObject::new
SceneObject::new(mesh, transform) 构造一个对象;同一个网格可以被多个对象共享。
pub fn SceneObject::new(@core.Mesh, @core.Transform3) -> Self
Light
Light 是方向光:direction 是世界空间中从场景指向光源的单位向量。
pub struct Light {
direction : @mutable.Vector[Double]
}
Light::directional
Light::directional(direction) 构造一个光源,并对 direction 归一化。
pub fn Light::directional(@mutable.Vector[Double]) -> Self
零向量会得到一个什么也照不亮的光源。
Light::default
Light::default() 即 Light::directional((0.6, 0.7, -1.0)):来自右上方,与默认相机位于同一侧。
pub fn Light::default() -> Self
它是普通的构造函数,而不是 Default trait 的实现。
Scene
Scene 是由一个方向光照亮的对象列表。
pub struct Scene {
objects : Array[SceneObject]
light : Light
}
Scene::new, Scene::single
Scene::new(light) 构造空场景;Scene::single(mesh, transform, light) 构造只含一个对象的场景。
pub fn Scene::new(Light) -> Self
pub fn Scene::single(@core.Mesh, @core.Transform3, Light) -> Self
Scene::add_object
scene.add_object(object) 原地把对象追加到场景中。
pub fn Scene::add_object(Self, SceneObject) -> Unit
test "scene" {
let scene = @frontend.Scene::new(@frontend.Light::default())
scene.add_object(
@frontend.SceneObject::new(@core.cube_mesh(1.0), @core.Transform3::identity()),
)
scene.add_object(
@frontend.SceneObject::new(
@core.sphere_mesh(0.5, 8, 12),
@core.Transform3::translation(2.0, 0.0, 0.0),
),
)
inspect(scene.objects.length(), content="2")
inspect(@core.vec_length(scene.light.direction), content="1")
}
渲染视图与绘制列表
RenderView
RenderView 是渲染场景时使用的相机和透视投影。
pub struct RenderView {
camera : @view.Camera3
projection : @view.PerspectiveProjection
}
RenderView::perspective, RenderView::scientific
RenderView::perspective(camera, projection) 把相机与投影配对;RenderView::scientific(camera, viewport) 取 ScientificCamera 的相机,并根据其镜头和传感器推导投影。
pub fn RenderView::perspective(@view.Camera3, @view.PerspectiveProjection) -> Self
pub fn RenderView::scientific(@view.ScientificCamera, @view.Viewport) -> Self
DrawTriangle
DrawTriangle 是一个带平面亮度的已投影三角形,亮度名义上位于 ;舍入可能使其超过 1 几个 ulp,后端会对其截断。
pub struct DrawTriangle {
p0 : @view.ProjectedVertex
p1 : @view.ProjectedVertex
p2 : @view.ProjectedVertex
intensity : Double
}
顶点采用视口单位,深度为相机空间深度,与 PerspectiveProjection 的输出一致。屏幕上的环绕方向未经规范化;光栅化器接受两种方向。
DrawTriangle::new
DrawTriangle::new(p0, p1, p2, intensity) 构造一个三角形,例如用于在没有场景的情况下驱动后端。
pub fn DrawTriangle::new(@view.ProjectedVertex, @view.ProjectedVertex, @view.ProjectedVertex, Double) -> Self
DrawList
DrawList 是每个后端都会消费的有序三角形列表。
pub struct DrawList {
triangles : Array[DrawTriangle]
}
DrawList::new, DrawList::push_triangle
DrawList::new() 构造空列表,list.push_triangle(t) 原地追加一个三角形。
pub fn DrawList::new() -> Self
pub fn DrawList::push_triangle(Self, DrawTriangle) -> Unit
build_draw_list
build_draw_list(scene, view) 运行几何管线:模型变换、阴影贴图、视图变换、投影、背面剔除、带阴影的朗伯着色以及三角化。
pub fn build_draw_list(Scene, RenderView) -> DrawList
对于每个朝向相机的面,它推入两个三角形(对于退化四边形,第二个面积为零),亮度为
其中 是根据 128 × 128 阴影贴图,面的中心与四个顶点中光线能照到的比例。背向光源的面得到 。三角形按场景顺序逐个对象、在对象内逐个面排列;列表不按深度排序。开销与顶点数和面数成线性关系,另加阴影贴图的光栅化。
test "draw list" {
let scene = @frontend.Scene::single(
@core.cube_mesh(1.0),
@core.Transform3::rotation(0.4, 0.6, 0.0),
@frontend.Light::default(),
)
let view = @frontend.RenderView::perspective(
@view.Camera3::default(4.5),
@view.PerspectiveProjection::new(@view.Viewport::new(80, 40), 30.0),
)
let list = @frontend.build_draw_list(scene, view)
// three faces of a turned cube face the camera, two triangles each
inspect(list.triangles.length(), content="6")
inspect(list.triangles.all(fn(t) { t.intensity >= 0.0 && t.intensity <= 1.0 }), content="true")
}
亮度缓冲
LUMA_FAR_DEPTH
LUMA_FAR_DEPTH 是空像素的深度,。
pub const LUMA_FAR_DEPTH : Double = 1.0e30
深度至少为 LUMA_FAR_DEPTH * 0.5 的像素会被后端视为背景。
LumaBuffer
LumaBuffer 是按行主序存储的标量亮度图像,每个像素带一个深度。
pub struct LumaBuffer {
width : Int
height : Int
values : Array[Double]
depths : Array[Double]
}
LumaBuffer::new
LumaBuffer::new(width, height) 构造一个缓冲,所有值为 0.0、所有深度为 LUMA_FAR_DEPTH。
pub fn LumaBuffer::new(Int, Int) -> Self
LumaBuffer::index
buffer.index(x, y) 返回数组下标 y * width + x,不做边界检查。
pub fn LumaBuffer::index(Self, Int, Int) -> Int
LumaBuffer::get, LumaBuffer::depth_at
buffer.get(x, y) 返回某个像素的值,buffer.depth_at(x, y) 返回其深度;在缓冲之外分别返回 0.0 和 LUMA_FAR_DEPTH。
pub fn LumaBuffer::get(Self, Int, Int) -> Double
pub fn LumaBuffer::depth_at(Self, Int, Int) -> Double
LumaBuffer::set_if_closer
buffer.set_if_closer(x, y, depth, value) 即深度测试:仅当 depth + DEPTH_EPSILON 小于已存深度时才写入该像素。
pub fn LumaBuffer::set_if_closer(Self, Int, Int, Double, Double) -> Unit
缓冲之外的写入会被忽略。深度相等时保留先写入的值。
LumaBuffer::draw_triangle
buffer.draw_triangle(t) 光栅化一个三角形:中心 位于三角形内部或边上的每个像素,若用透视校正深度通过深度测试,就得到 t.intensity。
pub fn LumaBuffer::draw_triangle(Self, DrawTriangle) -> Unit
面积不超过 DEPTH_EPSILON 的三角形会被跳过。循环遍历三角形的包围盒,该包围盒不会被裁剪到缓冲范围内。
draw_list_to_luma
draw_list_to_luma(list, width, height) 创建一个缓冲,并按顺序把列表中的每个三角形绘制进去。
pub fn draw_list_to_luma(DrawList, Int, Int) -> LumaBuffer
LumaBuffer::add_weighted_sample
buffer.add_weighted_sample(sample, w) 原地把 w * sample.values[i] 加到每个值上,并保留两者中较小的深度。
pub fn LumaBuffer::add_weighted_sample(Self, Self, Double) -> Unit
只处理两个长度中较小者()对应的前缀;请使用相同尺寸的缓冲。
average_luma
average_luma(a, b) 对两个相同尺寸的缓冲返回一个新缓冲,其值为两者的平均,深度为两者的最小值。
pub fn average_luma(LumaBuffer, LumaBuffer) -> LumaBuffer
test "luma buffer" {
let near = @frontend.DrawTriangle::new(
@view.ProjectedVertex::new(0.0, 0.0, 2.0),
@view.ProjectedVertex::new(8.0, 0.0, 2.0),
@view.ProjectedVertex::new(0.0, 8.0, 2.0),
0.8,
)
let far = @frontend.DrawTriangle::new(
@view.ProjectedVertex::new(0.0, 0.0, 5.0),
@view.ProjectedVertex::new(8.0, 0.0, 5.0),
@view.ProjectedVertex::new(0.0, 8.0, 5.0),
0.3,
)
let list = @frontend.DrawList::new()
list.push_triangle(far)
list.push_triangle(near)
let buffer = @frontend.draw_list_to_luma(list, 8, 8)
inspect(buffer.get(1, 1), content="0.8")
inspect(buffer.depth_at(1, 1), content="2")
inspect(buffer.depth_at(7, 7) == @frontend.LUMA_FAR_DEPTH, content="true")
let dark = @frontend.LumaBuffer::new(8, 8)
inspect(@frontend.average_luma(buffer, dark).get(1, 1), content="0.4")
dark.add_weighted_sample(buffer, 0.5)
inspect(dark.get(1, 1), content="0.4")
}
曝光
ShutterSpeed
ShutterSpeed 是以秒为单位的曝光时间。
pub struct ShutterSpeed {
seconds : Double
}
ShutterSpeed::seconds, ShutterSpeed::reciprocal
ShutterSpeed::seconds(t) 构造 t 秒的快门,ShutterSpeed::reciprocal(n) 构造 1/n 秒的快门;非正的参数得到 1/60 秒。
pub fn ShutterSpeed::seconds(Double) -> Self
pub fn ShutterSpeed::reciprocal(Double) -> Self
ExposureSettings
ExposureSettings 由快门、它所属的帧间隔以及需要平均的采样数组成。
pub struct ExposureSettings {
shutter : ShutterSpeed
frame_dt : Double
samples : Int
}
ExposureSettings::new
ExposureSettings::new(shutter, frame_dt, samples) 构造设置:非正的 frame_dt 变为 1/60 秒,快门被截断为不超过 frame_dt,samples 被提升到至少 1。
pub fn ExposureSettings::new(ShutterSpeed, Double, Int) -> Self
ExposureSettings::auto
ExposureSettings::auto(shutter, frame_dt) 构造 samples = ceil(shutter / frame_dt) 的设置,该值在快门被截断之后计算。
pub fn ExposureSettings::auto(ShutterSpeed, Double) -> Self
由于截断后快门永远不会超过 frame_dt,auto 总是只得到一个采样。需要长曝光的调用者应自行选择更大的采样数,TUI 演示就是这样做的。
test "exposure" {
let shutter = @frontend.ShutterSpeed::reciprocal(30.0)
let settings = @frontend.ExposureSettings::auto(shutter, 1.0 / 60.0)
inspect(settings.shutter.seconds == 1.0 / 60.0, content="true")
inspect(settings.samples, content="1")
let manual = @frontend.ExposureSettings::new(shutter, 1.0 / 24.0, 12)
inspect(manual.samples, content="12")
}
光流
FlowVector
FlowVector 是整数像素位移。
pub struct FlowVector {
dx : Int
dy : Int
}
FlowVector::new
FlowVector::new(dx, dy) 构造一个位移。
pub fn FlowVector::new(Int, Int) -> Self
FlowField
FlowField 是按行主序存储的位移场,每个像素一个位移。
pub struct FlowField {
width : Int
height : Int
vectors : Array[FlowVector]
}
FlowField::new, FlowField::index, FlowField::get, FlowField::set
FlowField::new(w, h) 构造零场;index 为 y * width + x;get 在场外返回 (0, 0),set 忽略场外的写入。
pub fn FlowField::new(Int, Int) -> Self
pub fn FlowField::index(Self, Int, Int) -> Int
pub fn FlowField::get(Self, Int, Int) -> FlowVector
pub fn FlowField::set(Self, Int, Int, FlowVector) -> Unit
estimate_optical_flow
estimate_optical_flow(previous, current, search_radius, patch_radius) 为 current 的每个像素估计其邻域来自 previous 中的何处。
pub fn estimate_optical_flow(LumaBuffer, LumaBuffer, Int, Int) -> FlowField
对每个像素 ,它返回满足 search_radius 的位移 ,使 previous 中以 为中心的 块与 current 中以 为中心的块之间的差平方和最小,其中 = patch_radius。缓冲之外的像素读作 0.0。并列时保留扫描顺序(先 后 ,从 开始递增)中的第一个位移,因此在每个位移都同样吻合的均匀区域中,结果是 而不是 。负半径按 0 处理。开销为 。
align_with_flow
align_with_flow(previous, current, flow) 把 previous 扭曲到 current 的像素网格上:结果的像素 取 previous 中 处的值和深度。
pub fn align_with_flow(LumaBuffer, LumaBuffer, FlowField) -> LumaBuffer
accumulate_with_flow
accumulate_with_flow(previous, current, flow) 返回扭曲后的 previous 与 current 的平均,深度取自 current。
pub fn accumulate_with_flow(LumaBuffer, LumaBuffer, FlowField) -> LumaBuffer
test "optical flow" {
let previous = @frontend.LumaBuffer::new(6, 6)
let current = @frontend.LumaBuffer::new(6, 6)
previous.set_if_closer(2, 2, 1.0, 1.0) // a bright pixel at (2, 2)
current.set_if_closer(3, 2, 1.0, 1.0) // has moved one pixel right
let flow = @frontend.estimate_optical_flow(previous, current, 2, 1)
let v = flow.get(3, 2)
debug_inspect((v.dx, v.dy), content="(-1, 0)")
let aligned = @frontend.align_with_flow(previous, current, flow)
inspect(aligned.get(3, 2), content="1")
inspect(@frontend.accumulate_with_flow(previous, current, flow).get(3, 2), content="1")
// far from the moving pixel every displacement fits: the first one wins
let flat = flow.get(0, 5)
debug_inspect((flat.dx, flat.dy), content="(-2, -2)")
}
时间轴
Timeline
Timeline 是固定帧率的片段:以秒为单位的时长和帧率。
pub struct Timeline {
duration_seconds : Double
fps : Int
}
Timeline::new
Timeline::new(duration, fps) 构造时间轴;非正的时长变为 1 秒,小于 1 的 fps 变为 1。
pub fn Timeline::new(Double, Int) -> Self
Timeline::frame_count, Timeline::frame_dt
frame_count 返回 ,frame_dt 返回 。
pub fn Timeline::frame_count(Self) -> Int
pub fn Timeline::frame_dt(Self) -> Double
TimelineSample
TimelineSample 是时间轴中的一帧:它的下标、时间 和进度 。
pub struct TimelineSample {
frame_index : Int
time_seconds : Double
progress : Double
}
Timeline::sample
timeline.sample(k) 返回第 k 帧的采样;负下标按 0 处理。允许超出末尾的下标,其进度为 1。
pub fn Timeline::sample(Self, Int) -> TimelineSample
ScalarKeyframe
ScalarKeyframe 是某一时刻的一个值。
pub struct ScalarKeyframe {
time_seconds : Double
value : Double
}
ScalarKeyframe::new
ScalarKeyframe::new(time, value) 构造一个关键帧。
pub fn ScalarKeyframe::new(Double, Double) -> Self
ScalarTrack
ScalarTrack 是一条穿过按时间排序的关键帧的分段线性动画曲线。
pub struct ScalarTrack {
keyframes : Array[ScalarKeyframe]
}
ScalarTrack::new
ScalarTrack::new(keyframes) 构造一条轨道;关键帧必须按时间递增排序。
pub fn ScalarTrack::new(Array[ScalarKeyframe]) -> Self
ScalarTrack::sample
track.sample(t) 返回时刻 t 的值:在相邻关键帧之间线性插值,第一个关键帧之前取第一个值,最后一个之后取最后一个值,空轨道返回 0.0。
pub fn ScalarTrack::sample(Self, Double) -> Double
当两个关键帧时间相同(相差在 DEPTH_EPSILON 以内)时,该时刻取后一个值。
test "timeline" {
let timeline = @frontend.Timeline::new(1.0, 4)
inspect(timeline.frame_count(), content="4")
let s = timeline.sample(2)
debug_inspect((s.time_seconds, s.progress), content="(0.5, 0.5)")
let track = @frontend.ScalarTrack::new([
@frontend.ScalarKeyframe::new(0.0, 0.0),
@frontend.ScalarKeyframe::new(1.0, 10.0),
])
inspect(track.sample(0.25), content="2.5")
inspect(track.sample(5.0), content="10")
}