///|
const FRAME_INTERVAL_MS : UInt64 = 33UL
///|
const ANGLE_STEP_X : Double = 0.05
///|
const ANGLE_STEP_Y : Double = 0.035
///|
const ANGLE_STEP_Z : Double = 0.02
///|
const DEFAULT_CAMERA_DISTANCE : Double = 4.5
///|
const DEFAULT_FOCAL_LENGTH_MM : Double = 18.0
///|
const TORUS_CAMERA_DISTANCE : Double = 6.8
///|
const TORUS_FOCAL_LENGTH_MM : Double = 18.0
///|
const HITCHCOCK_BASE_DISTANCE : Double = 4.5
///|
const HITCHCOCK_NEAR_DISTANCE : Double = 3.4
///|
const HITCHCOCK_FAR_DISTANCE : Double = 8.0
///|
const HITCHCOCK_BASE_FOCAL_MM : Double = 18.0
///|
const DOLLY_BASE_DISTANCE : Double = 3.2
///|
const DOLLY_NEAR_DISTANCE : Double = 3.2
///|
const DOLLY_FAR_DISTANCE : Double = 69.56521739130434
///|
const DOLLY_BASE_FOCAL_MM : Double = 23.0
///|
const DOLLY_WALL_Z : Double = 5.0
///|
const EXPOSURE_FRAME_DT : Double = 1.0 / 30.0
///|
const EXPOSURE_SHUTTER_DENOMINATOR : Double = 30.0
///|
const EXPOSURE_ACCUMULATION_SAMPLES : Int32 = 12
///|
fn has_arg(args : Array[String], name : String) -> Bool {
args.iter().any(fn(arg) { arg == name })
}
///|
fn arg_value(args : Array[String], name : String) -> String? {
for i in 0.. Int32 {
if ch == '0' {
0
} else if ch == '1' {
1
} else if ch == '2' {
2
} else if ch == '3' {
3
} else if ch == '4' {
4
} else if ch == '5' {
5
} else if ch == '6' {
6
} else if ch == '7' {
7
} else if ch == '8' {
8
} else if ch == '9' {
9
} else {
-1
}
}
///|
fn parse_positive_int(text : String, fallback : Int32) -> Int32 {
let mut value = 0
let mut seen_digit = false
for ch in text {
let digit = digit_value(ch)
if digit >= 0 {
value = value * 10 + digit
seen_digit = true
}
}
if seen_digit && value > 0 {
value
} else {
fallback
}
}
///|
fn env_positive_int(
envs : Map[String, String],
key : String,
fallback : Int32,
) -> Int32 {
match envs.get(key) {
Some(value) => parse_positive_int(value, fallback)
None => fallback
}
}
///|
fn auto_viewport() -> @view.Viewport {
let envs = @env.get_env_vars()
let width = env_positive_int(envs, "COLUMNS", @tui.DEFAULT_WIDTH)
let raw_height = env_positive_int(envs, "LINES", @tui.DEFAULT_HEIGHT + 1)
// Leave one row for the shell prompt to avoid scroll churn.
let height = if raw_height > 1 { raw_height - 1 } else { @tui.DEFAULT_HEIGHT }
@view.Viewport::new(width, height)
}
///|
fn auto_render_config() -> @tui.TuiRenderConfig {
let viewport = auto_viewport()
@tui.TuiRenderConfig::sized(viewport.width, viewport.height)
}
///|
fn timeline_from_args(args : Array[String]) -> @frontend.Timeline {
let duration = match arg_value(args, "--duration") {
Some(value) => parse_positive_int(value, 3).to_double()
None => 3.0
}
let fps = match arg_value(args, "--fps") {
Some(value) => parse_positive_int(value, 30)
None => 30
}
@frontend.Timeline::new(duration, fps)
}
///|
fn demo_mesh(args : Array[String]) -> @core.Mesh {
if has_arg(args, "--sphere") {
@core.sphere_mesh(2.4, 18, 36)
} else if has_arg(args, "--torus") {
@core.torus_mesh(2.1, 0.72, 28, 16)
} else {
@core.cube_mesh(1.0)
}
}
///|
fn demo_should_rotate(args : Array[String]) -> Bool {
!has_arg(args, "--sphere")
}
///|
fn default_render_view(
distance : Double,
focal_length_mm : Double,
) -> @frontend.RenderView {
let viewport = auto_viewport()
let camera = @view.Camera3::default(distance)
let scientific = @view.ScientificCamera::new(
camera,
@view.SensorSpec::full_frame(),
@view.LensSpec::new(focal_length_mm),
@view.WorldUnit::unitless(),
)
@frontend.RenderView::scientific(scientific, viewport)
}
///|
fn demo_render_view() -> @frontend.RenderView {
default_render_view(DEFAULT_CAMERA_DISTANCE, DEFAULT_FOCAL_LENGTH_MM)
}
///|
fn torus_render_view() -> @frontend.RenderView {
default_render_view(TORUS_CAMERA_DISTANCE, TORUS_FOCAL_LENGTH_MM)
}
///|
fn demo_render_view_for_args(args : Array[String]) -> @frontend.RenderView {
if has_arg(args, "--torus") {
torus_render_view()
} else {
demo_render_view()
}
}
///|
fn scaled_translated(
scale_x : Double,
scale_y : Double,
scale_z : Double,
x : Double,
y : Double,
z : Double,
) -> @core.Transform3 {
@core.Transform3::scale(scale_x, scale_y, scale_z).compose(
@core.Transform3::translation(x, y, z),
)
}
///|
fn hitchcock_distance(frame : Int32) -> Double {
let t = @math.sin(frame.to_double() * 0.035)
let midpoint = (HITCHCOCK_NEAR_DISTANCE + HITCHCOCK_FAR_DISTANCE) * 0.5
let amplitude = (HITCHCOCK_FAR_DISTANCE - HITCHCOCK_NEAR_DISTANCE) * 0.5
midpoint + amplitude * t
}
///|
fn hitchcock_focal_length_mm(distance : Double) -> Double {
HITCHCOCK_BASE_FOCAL_MM * distance / HITCHCOCK_BASE_DISTANCE
}
///|
fn hitchcock_render_view(frame : Int32) -> @frontend.RenderView {
let distance = hitchcock_distance(frame)
let viewport = auto_viewport()
let camera = @view.Camera3::default(distance)
let scientific = @view.ScientificCamera::new(
camera,
@view.SensorSpec::full_frame(),
@view.LensSpec::new(hitchcock_focal_length_mm(distance)),
@view.WorldUnit::unitless(),
)
@frontend.RenderView::scientific(scientific, viewport)
}
///|
fn dolly_distance(frame : Int32) -> Double {
let wave = (@math.sin(frame.to_double() * 0.028) + 1.0) * 0.5
DOLLY_NEAR_DISTANCE + (DOLLY_FAR_DISTANCE - DOLLY_NEAR_DISTANCE) * wave
}
///|
fn dolly_focal_length_mm(distance : Double) -> Double {
DOLLY_BASE_FOCAL_MM * distance / DOLLY_BASE_DISTANCE
}
///|
fn dolly_render_view(frame : Int32) -> @frontend.RenderView {
let distance = dolly_distance(frame)
let viewport = auto_viewport()
let camera = @view.Camera3::default(distance)
let scientific = @view.ScientificCamera::new(
camera,
@view.SensorSpec::full_frame(),
@view.LensSpec::new(dolly_focal_length_mm(distance)),
@view.WorldUnit::unitless(),
)
@frontend.RenderView::scientific(scientific, viewport)
}
///|
fn dolly_scene(frame : Int32) -> @frontend.Scene {
let scene = @frontend.Scene::new(
@frontend.Light::directional(@core.vec3(-0.35, 0.7, -1.0)),
)
let time = frame.to_double()
scene.add_object(
@frontend.SceneObject::new(
@core.cube_mesh(1.0),
@core.Transform3::rotation(time * 0.041, time * 0.057, time * 0.023).compose(
@core.Transform3::scale(1.35, 1.35, 1.35),
),
),
)
let disc = @core.cylinder_mesh(0.58, 0.10, 20)
for row in 0..<5 {
for column in 0..<7 {
let x = (column - 3).to_double() * 2.32
let y = (2 - row).to_double() * 2.32
let z = DOLLY_WALL_Z
scene.add_object(
@frontend.SceneObject::new(
disc,
@core.Transform3::rotation(@math.PI * 0.5, 0.0, 0.0).compose(
scaled_translated(1.0, 1.0, 1.0, x, y, z),
),
),
)
}
}
scene
}
///|
fn hitchcock_scene() -> @frontend.Scene {
let scene = @frontend.Scene::new(
@frontend.Light::directional(@core.vec3(0.5, 0.8, -1.0)),
)
let cylinder = @core.cylinder_mesh(1.0, 2.4, 28)
let cone = @core.cone_mesh(1.15, 2.6, 32)
let pyramid = @core.triangular_pyramid_mesh(1.35, 2.7)
scene.add_object(
@frontend.SceneObject::new(
@core.cube_mesh(1.0),
@core.Transform3::rotation(0.62, -0.79, 0.79).compose(
@core.Transform3::scale(1.55, 1.55, 1.55),
),
),
)
scene.add_object(
@frontend.SceneObject::new(
cylinder,
@core.Transform3::rotation(0.25, 0.0, 0.55).compose(
scaled_translated(1.4, 1.5, 1.4, -4.2, 0.9, 2.8),
),
),
)
scene.add_object(
@frontend.SceneObject::new(
cone,
@core.Transform3::rotation(-0.2, 0.35, -0.25).compose(
scaled_translated(1.45, 1.65, 1.45, 2.8, 0.6, 3.1),
),
),
)
scene.add_object(
@frontend.SceneObject::new(
pyramid,
@core.Transform3::rotation(0.1, -0.5, 0.25).compose(
scaled_translated(1.65, 1.7, 1.65, 0.0, 1.8, 4.2),
),
),
)
scene.add_object(
@frontend.SceneObject::new(
cylinder,
@core.Transform3::rotation(1.25, 0.4, -0.15).compose(
scaled_translated(1.2, 1.25, 1.2, -1.8, -1.35, 3.7),
),
),
)
scene.add_object(
@frontend.SceneObject::new(
cone,
@core.Transform3::rotation(0.75, -0.55, 0.35).compose(
scaled_translated(1.2, 1.35, 1.2, 4.6, -1.05, 4.3),
),
),
)
scene
}
///|
fn mesh_frame_with_view(
mesh : @core.Mesh,
angle_x : Double,
angle_y : Double,
angle_z : Double,
render_view : @frontend.RenderView,
) -> String {
let transform = @core.Transform3::rotation(angle_x, angle_y, angle_z)
let scene = @frontend.Scene::single(
mesh,
transform,
@frontend.Light::default(),
)
let draw_list = @frontend.build_draw_list(scene, render_view)
@tui.render_frame(draw_list, auto_render_config())
}
///|
fn mesh_luma_with_view(
mesh : @core.Mesh,
angle_x : Double,
angle_y : Double,
angle_z : Double,
render_view : @frontend.RenderView,
) -> @frontend.LumaBuffer {
let transform = @core.Transform3::rotation(angle_x, angle_y, angle_z)
let scene = @frontend.Scene::single(
mesh,
transform,
@frontend.Light::default(),
)
let draw_list = @frontend.build_draw_list(scene, render_view)
@tui.draw_list_to_tui_luma(draw_list, auto_render_config())
}
///|
fn hitchcock_frame(frame : Int32) -> String {
let draw_list = @frontend.build_draw_list(
hitchcock_scene(),
hitchcock_render_view(frame),
)
@tui.render_frame(draw_list, auto_render_config())
}
///|
fn hitchcock_luma(frame : Int32) -> @frontend.LumaBuffer {
let draw_list = @frontend.build_draw_list(
hitchcock_scene(),
hitchcock_render_view(frame),
)
@tui.draw_list_to_tui_luma(draw_list, auto_render_config())
}
///|
fn dolly_frame(frame : Int32) -> String {
let draw_list = @frontend.build_draw_list(
dolly_scene(frame),
dolly_render_view(frame),
)
@tui.render_frame(draw_list, auto_render_config())
}
///|
fn dolly_luma(frame : Int32) -> @frontend.LumaBuffer {
let draw_list = @frontend.build_draw_list(
dolly_scene(frame),
dolly_render_view(frame),
)
@tui.draw_list_to_tui_luma(draw_list, auto_render_config())
}
///|
fn add_exposure_sample(
out : @frontend.LumaBuffer,
sample : @frontend.LumaBuffer,
current : @frontend.LumaBuffer,
weight : Double,
use_flow : Bool,
) -> Unit {
if use_flow {
let flow = @frontend.estimate_optical_flow(sample, current, 4, 1)
out.add_weighted_sample(
@frontend.align_with_flow(sample, current, flow),
weight,
)
} else {
out.add_weighted_sample(sample, weight)
}
}
///|
fn exposure_mesh_frame_with_view(
mesh : @core.Mesh,
angle_x : Double,
angle_y : Double,
angle_z : Double,
use_flow : Bool,
render_view : @frontend.RenderView,
) -> String {
let viewport = auto_viewport()
let exposure = @frontend.ExposureSettings::auto(
@frontend.ShutterSpeed::reciprocal(EXPOSURE_SHUTTER_DENOMINATOR),
EXPOSURE_FRAME_DT,
)
let current = mesh_luma_with_view(
mesh, angle_x, angle_y, angle_z, render_view,
)
let accumulated = @frontend.LumaBuffer::new(viewport.width, viewport.height)
let sample_count = if exposure.samples > EXPOSURE_ACCUMULATION_SAMPLES {
exposure.samples
} else {
EXPOSURE_ACCUMULATION_SAMPLES
}
let weight = 1.0 / sample_count.to_double()
for i in 0.. String {
let viewport = auto_viewport()
let exposure = @frontend.ExposureSettings::auto(
@frontend.ShutterSpeed::reciprocal(EXPOSURE_SHUTTER_DENOMINATOR),
EXPOSURE_FRAME_DT,
)
let current = hitchcock_luma(frame)
let accumulated = @frontend.LumaBuffer::new(viewport.width, viewport.height)
let sample_count = if exposure.samples > EXPOSURE_ACCUMULATION_SAMPLES {
exposure.samples
} else {
EXPOSURE_ACCUMULATION_SAMPLES
}
let weight = 1.0 / sample_count.to_double()
for i in 0.. String {
let viewport = auto_viewport()
let exposure = @frontend.ExposureSettings::auto(
@frontend.ShutterSpeed::reciprocal(EXPOSURE_SHUTTER_DENOMINATOR),
EXPOSURE_FRAME_DT,
)
let current = dolly_luma(frame)
let accumulated = @frontend.LumaBuffer::new(viewport.width, viewport.height)
let sample_count = if exposure.samples > EXPOSURE_ACCUMULATION_SAMPLES {
exposure.samples
} else {
EXPOSURE_ACCUMULATION_SAMPLES
}
let weight = 1.0 / sample_count.to_double()
for i in 0.. String {
let angle_x = sample.time_seconds * 1.5
let angle_y = sample.time_seconds * 1.05
let angle_z = sample.time_seconds * 0.6
mesh_frame_with_view(mesh, angle_x, angle_y, angle_z, render_view)
}
///|
fn timeline_exposure_mesh_frame_with_view(
mesh : @core.Mesh,
sample : @frontend.TimelineSample,
use_flow : Bool,
render_view : @frontend.RenderView,
) -> String {
let angle_x = sample.time_seconds * 1.5
let angle_y = sample.time_seconds * 1.05
let angle_z = sample.time_seconds * 0.6
exposure_mesh_frame_with_view(
mesh, angle_x, angle_y, angle_z, use_flow, render_view,
)
}
///|
fn render_demo_frame(
args : Array[String],
frame_index : Int32,
sample : @frontend.TimelineSample,
) -> String {
let exposure = has_arg(args, "--long-exposure") ||
has_arg(args, "--flow-exposure")
let use_flow = has_arg(args, "--flow-exposure")
let mesh = demo_mesh(args)
let render_view = demo_render_view_for_args(args)
if has_arg(args, "--dolly") {
if exposure {
exposure_dolly_frame(frame_index, use_flow)
} else {
dolly_frame(frame_index)
}
} else if has_arg(args, "--hitchcock") {
if exposure {
exposure_hitchcock_frame(frame_index, use_flow)
} else {
hitchcock_frame(frame_index)
}
} else if exposure {
timeline_exposure_mesh_frame_with_view(mesh, sample, use_flow, render_view)
} else {
timeline_mesh_frame_with_view(mesh, sample, render_view)
}
}
///|
fn render_demo_sequence(
args : Array[String],
timeline : @frontend.Timeline,
) -> @tui.TuiSequence {
let config = auto_render_config()
let sequence = @tui.TuiSequence::new(
config.width,
config.height,
timeline.fps,
)
for frame_index in 0.. Unit {
let config = auto_render_config()
println("GEOMETRY3D_TUI_SEQUENCE v1")
println("width=\{config.width}")
println("height=\{config.height}")
println("fps=\{timeline.fps}")
println("frames=\{timeline.frame_count()}")
for frame_index in 0.. @tui.TuiImage {
let config = auto_render_config()
let exposure = has_arg(args, "--long-exposure") ||
has_arg(args, "--flow-exposure")
let use_flow = has_arg(args, "--flow-exposure")
let render_view = demo_render_view_for_args(args)
let content = if has_arg(args, "--dolly") {
if exposure {
exposure_dolly_frame(1, use_flow)
} else {
dolly_frame(0)
}
} else if has_arg(args, "--hitchcock") {
if exposure {
exposure_hitchcock_frame(1, use_flow)
} else {
hitchcock_frame(0)
}
} else if exposure {
exposure_mesh_frame_with_view(
demo_mesh(args),
0.2,
0.3,
0.0,
use_flow,
render_view,
)
} else {
mesh_frame_with_view(demo_mesh(args), 0.2, 0.3, 0.0, render_view)
}
@tui.TuiImage::new(config.width, config.height, content)
}
///|
fn frame_interval_ms_for_fps(fps : Int32) -> UInt64 {
if fps >= 60 {
16UL
} else if fps >= 30 {
33UL
} else if fps >= 24 {
41UL
} else if fps >= 12 {
83UL
} else {
100UL
}
}
///|
fn play_sequence(sequence : @tui.TuiSequence, frame_limit : Int32?) -> Unit {
let interval = frame_interval_ms_for_fps(sequence.fps)
let mut last_tick = @env.now()
let mut rendered = 0
while rendered < sequence.frames.length() {
let now = @env.now()
if now - last_tick < interval {
continue
}
last_tick = now
println("\u{1b}[2J\u{1b}[H\{sequence.frames[rendered].content}")
rendered += 1
match frame_limit {
Some(limit) if rendered >= limit => break
_ => ()
}
}
}
///|
fn record_sequence(args : Array[String], path : String) -> Unit {
let sequence = render_demo_sequence(args, timeline_from_args(args))
@fs.write_string_to_file(path, @tui.encode_tui_sequence(sequence)) catch {
_ => abort("failed to write TUI sequence: " + path)
}
}
///|
fn play_sequence_file(path : String, frame_limit : Int32?) -> Unit {
let content = @fs.read_file_to_string(path) catch {
_ => abort("failed to read TUI sequence: " + path)
}
play_sequence(@tui.decode_tui_sequence(content), frame_limit)
}
///|
fn export_image(args : Array[String], path : String) -> Unit {
@fs.write_string_to_file(path, @tui.encode_tui_image(render_demo_image(args))) catch {
_ => abort("failed to write TUI image: " + path)
}
}
///|
fn show_image_file(path : String) -> Unit {
let content = @fs.read_file_to_string(path) catch {
_ => abort("failed to read TUI image: " + path)
}
let image = @tui.decode_tui_image(content)
println("\u{1b}[2J\u{1b}[H\{image.content}")
}
///|
fn render_once_with_view(
mesh : @core.Mesh,
angle_x : Double,
angle_y : Double,
angle_z : Double,
render_view : @frontend.RenderView,
) -> Unit {
println(
"\u{1b}[2J\u{1b}[H\{mesh_frame_with_view(mesh, angle_x, angle_y, angle_z, render_view)}",
)
}
///|
fn render_exposure_once_with_view(
mesh : @core.Mesh,
angle_x : Double,
angle_y : Double,
angle_z : Double,
use_flow : Bool,
render_view : @frontend.RenderView,
) -> Unit {
println(
"\u{1b}[2J\u{1b}[H\{exposure_mesh_frame_with_view(mesh, angle_x, angle_y, angle_z, use_flow, render_view)}",
)
}
///|
fn render_hitchcock_once(frame : Int32) -> Unit {
println("\u{1b}[2J\u{1b}[H\{hitchcock_frame(frame)}")
}
///|
fn render_hitchcock_exposure_once(frame : Int32, use_flow : Bool) -> Unit {
println("\u{1b}[2J\u{1b}[H\{exposure_hitchcock_frame(frame, use_flow)}")
}
///|
fn render_dolly_once(frame : Int32) -> Unit {
println("\u{1b}[2J\u{1b}[H\{dolly_frame(frame)}")
}
///|
fn render_dolly_exposure_once(frame : Int32, use_flow : Bool) -> Unit {
println("\u{1b}[2J\u{1b}[H\{exposure_dolly_frame(frame, use_flow)}")
}
///|
fn run_animation(
mesh : @core.Mesh,
rotate : Bool,
frame_limit : Int32?,
exposure : Bool,
use_flow : Bool,
render_view : @frontend.RenderView,
) -> Unit {
let mut angle_x = 0.0
let mut angle_y = 0.0
let mut angle_z = 0.0
let mut last_tick = @env.now()
let mut rendered = 0
while true {
let now = @env.now()
if now - last_tick < FRAME_INTERVAL_MS {
continue
}
last_tick = now
if exposure {
render_exposure_once_with_view(
mesh, angle_x, angle_y, angle_z, use_flow, render_view,
)
} else {
render_once_with_view(mesh, angle_x, angle_y, angle_z, render_view)
}
if rotate {
angle_x += ANGLE_STEP_X
angle_y += ANGLE_STEP_Y
angle_z += ANGLE_STEP_Z
}
rendered += 1
match frame_limit {
Some(limit) if rendered >= limit => break
_ => ()
}
}
}
///|
fn run_hitchcock(
frame_limit : Int32?,
exposure : Bool,
use_flow : Bool,
) -> Unit {
let mut last_tick = @env.now()
let mut rendered = 0
while true {
let now = @env.now()
if now - last_tick < FRAME_INTERVAL_MS {
continue
}
last_tick = now
if exposure {
render_hitchcock_exposure_once(rendered, use_flow)
} else {
render_hitchcock_once(rendered)
}
rendered += 1
match frame_limit {
Some(limit) if rendered >= limit => break
_ => ()
}
}
}
///|
fn run_dolly(frame_limit : Int32?, exposure : Bool, use_flow : Bool) -> Unit {
let mut last_tick = @env.now()
let mut rendered = 0
while true {
let now = @env.now()
if now - last_tick < FRAME_INTERVAL_MS {
continue
}
last_tick = now
if exposure {
render_dolly_exposure_once(rendered, use_flow)
} else {
render_dolly_once(rendered)
}
rendered += 1
match frame_limit {
Some(limit) if rendered >= limit => break
_ => ()
}
}
}
///|
fn main {
let args = @env.args()
let frame_limit = if has_arg(args, "--once") { Some(1) } else { None }
let exposure = has_arg(args, "--long-exposure") ||
has_arg(args, "--flow-exposure")
let use_flow = has_arg(args, "--flow-exposure")
let render_view = demo_render_view_for_args(args)
if has_arg(args, "--record-stdout") {
stream_demo_sequence(args, timeline_from_args(args))
return
}
match arg_value(args, "--show-image") {
Some(path) => show_image_file(path)
None =>
match arg_value(args, "--export-image") {
Some(path) => export_image(args, path)
None =>
match arg_value(args, "--play") {
Some(path) => play_sequence_file(path, frame_limit)
None =>
match arg_value(args, "--record") {
Some(path) => record_sequence(args, path)
None if has_arg(args, "--dolly") =>
run_dolly(frame_limit, exposure, use_flow)
None if has_arg(args, "--hitchcock") =>
run_hitchcock(frame_limit, exposure, use_flow)
None =>
run_animation(
demo_mesh(args),
demo_should_rotate(args),
frame_limit,
exposure,
use_flow,
render_view,
)
}
}
}
}
}