Skip to content

Commit 59902d5

Browse files
committed
add 3d support
1 parent 6a5b1be commit 59902d5

8 files changed

Lines changed: 1023 additions & 7 deletions

File tree

backend/src/main.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pub mod ffmpeg;
33
pub mod future;
44
pub mod util;
55

6-
use std::{net::SocketAddr, ops::Bound, sync::atomic::AtomicBool};
6+
use std::{net::SocketAddr, ops::Bound, sync::atomic::AtomicBool, time::{SystemTime, UNIX_EPOCH}};
77

88
use axum::{
99
Router,
@@ -143,13 +143,33 @@ struct AudioPlanResolved {
143143
loudness: Option<AudioLoudnessPreset>,
144144
}
145145

146+
#[derive(Deserialize)]
147+
struct RenderLogRequest {
148+
message: String,
149+
level: Option<String>,
150+
session: Option<String>,
151+
context: Option<serde_json::Value>,
152+
}
153+
154+
#[derive(Serialize, Clone)]
155+
struct RenderLogEntry {
156+
timestamp_ms: u64,
157+
message: String,
158+
level: String,
159+
session: Option<String>,
160+
context: Option<serde_json::Value>,
161+
}
162+
146163
static RENDER_AUDIO_PLAN: std::sync::LazyLock<std::sync::Mutex<Option<AudioPlanResolved>>> =
147164
std::sync::LazyLock::new(|| std::sync::Mutex::new(None));
165+
static RENDER_LOGS: std::sync::LazyLock<std::sync::Mutex<Vec<RenderLogEntry>>> =
166+
std::sync::LazyLock::new(|| std::sync::Mutex::new(Vec::new()));
148167

149168
static RENDER_COMPLETED: AtomicUsize = AtomicUsize::new(0);
150169
static RENDER_TOTAL: AtomicUsize = AtomicUsize::new(0);
151170
static RENDER_CANCEL: AtomicBool = AtomicBool::new(false);
152171
static NEXT_SESSION_ID: AtomicUsize = AtomicUsize::new(1);
172+
const MAX_RENDER_LOGS: usize = 2000;
153173

154174
#[tokio::main]
155175
async fn main() {
@@ -183,6 +203,12 @@ async fn main() {
183203
.get(get_progress_handler)
184204
.options(options_handler),
185205
)
206+
.route(
207+
"/render_log",
208+
post(render_log_handler)
209+
.get(get_render_log_handler)
210+
.options(options_handler),
211+
)
186212
.route(
187213
"/render_cancel",
188214
post(render_cancel_handler).options(options_handler),
@@ -597,6 +623,59 @@ async fn get_progress_handler(State(_state): State<AppState>) -> impl IntoRespon
597623
(headers, Json(response))
598624
}
599625

626+
fn now_ms() -> u64 {
627+
SystemTime::now()
628+
.duration_since(UNIX_EPOCH)
629+
.map(|d| d.as_millis() as u64)
630+
.unwrap_or(0)
631+
}
632+
633+
async fn render_log_handler(
634+
State(_state): State<AppState>,
635+
Json(payload): Json<RenderLogRequest>,
636+
) -> impl IntoResponse {
637+
let mut headers = HeaderMap::new();
638+
apply_cors(&mut headers);
639+
640+
let entry = RenderLogEntry {
641+
timestamp_ms: now_ms(),
642+
message: payload.message,
643+
level: payload.level.unwrap_or_else(|| "info".to_string()),
644+
session: payload.session,
645+
context: payload.context,
646+
};
647+
648+
{
649+
let mut logs = RENDER_LOGS.lock().unwrap();
650+
logs.push(entry.clone());
651+
if logs.len() > MAX_RENDER_LOGS {
652+
let trim = logs.len() - MAX_RENDER_LOGS;
653+
logs.drain(0..trim);
654+
}
655+
}
656+
657+
let session = entry.session.as_deref().unwrap_or("-");
658+
let context = entry
659+
.context
660+
.as_ref()
661+
.map(|value| value.to_string())
662+
.unwrap_or_default();
663+
info!(
664+
"[render_log:{}] {} session={} context={}",
665+
entry.level, entry.message, session, context
666+
);
667+
668+
(headers, StatusCode::OK)
669+
}
670+
671+
async fn get_render_log_handler(State(_state): State<AppState>) -> impl IntoResponse {
672+
let mut headers = HeaderMap::new();
673+
apply_cors(&mut headers);
674+
675+
let logs = RENDER_LOGS.lock().unwrap().clone();
676+
(headers, Json(logs))
677+
}
678+
600679
async fn render_cancel_handler(State(_state): State<AppState>) -> impl IntoResponse {
601680
let mut headers = HeaderMap::new();
602681
apply_cors(&mut headers);
@@ -617,6 +696,7 @@ async fn reset_handler(State(_state): State<AppState>) -> impl IntoResponse {
617696
DECODER.clear().await;
618697
RENDER_CANCEL.store(false, Ordering::Relaxed);
619698
*RENDER_AUDIO_PLAN.lock().unwrap() = None;
699+
RENDER_LOGS.lock().unwrap().clear();
620700
(headers, StatusCode::OK)
621701
}
622702

docs/docs/lib/3d.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
title: 3D
3+
sidebar_position: 5
4+
---
5+
6+
FrameScript provides WebGL helpers and a Three.js canvas wrapper that survives context loss.
7+
Render mode waits for recovery so incomplete frames are avoided.
8+
9+
## WebGL helpers
10+
11+
- `useWebGLContext` initializes WebGL, handles `webglcontextlost`, and recreates resources.
12+
- `useWebGLFrameWaiter` waits for GPU completion per frame during headless rendering.
13+
14+
```tsx
15+
import { useRef } from "react"
16+
import { useWebGLContext, useWebGLFrameWaiter } from "../src/lib/webgl"
17+
18+
const Canvas = () => {
19+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
20+
const { glRef } = useWebGLContext(canvasRef, ({ gl }) => {
21+
// init shaders/buffers
22+
return () => {
23+
// dispose resources
24+
}
25+
})
26+
27+
useWebGLFrameWaiter(glRef)
28+
29+
return <canvas ref={canvasRef} />
30+
}
31+
```
32+
33+
## ThreeCanvas
34+
35+
`<ThreeCanvas />` integrates with `useAnimation` by sampling variables in the `update` callback.
36+
37+
```tsx
38+
import { useAnimation, useVariable } from "../src/lib/animation"
39+
import { BEZIER_SMOOTH } from "../src/lib/animation/functions"
40+
import { seconds } from "../src/lib/frame"
41+
import { ThreeCanvas, THREE, disposeThreeObject } from "../src/lib/webgl/three"
42+
43+
const Scene = () => {
44+
const progress = useVariable(0)
45+
46+
useAnimation(async (ctx) => {
47+
await ctx.move(progress).to(1, seconds(2), BEZIER_SMOOTH)
48+
await ctx.move(progress).to(0, seconds(2), BEZIER_SMOOTH)
49+
}, [])
50+
51+
return (
52+
<ThreeCanvas
53+
setup={({ renderer, size }) => {
54+
renderer.outputColorSpace = THREE.SRGBColorSpace
55+
56+
const scene = new THREE.Scene()
57+
const camera = new THREE.PerspectiveCamera(45, size.cssWidth / size.cssHeight, 0.1, 100)
58+
camera.position.z = 6
59+
60+
const mesh = new THREE.Mesh(
61+
new THREE.BoxGeometry(),
62+
new THREE.MeshStandardMaterial({ color: 0x44aa88 })
63+
)
64+
scene.add(mesh)
65+
66+
const light = new THREE.DirectionalLight(0xffffff, 1)
67+
light.position.set(2, 3, 4)
68+
scene.add(light)
69+
70+
return {
71+
scene,
72+
camera,
73+
update: ({ frame }) => {
74+
const t = progress.get(frame)
75+
mesh.position.x = (t - 0.5) * 3
76+
mesh.rotation.y = t * Math.PI * 2
77+
},
78+
dispose: () => disposeThreeObject(mesh),
79+
}
80+
}}
81+
/>
82+
)
83+
}
84+
```
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
title: 3D
3+
sidebar_position: 5
4+
---
5+
6+
FrameScript には WebGL 用のヘルパーと、Three.js の描画を扱うキャンバスが用意されています。
7+
context lost からの復旧も組み込まれており、レンダー時は復旧が完了するまで待機します。
8+
9+
## WebGL ヘルパー
10+
11+
- `useWebGLContext` が WebGL を初期化し、`webglcontextlost` からの復旧を行います。
12+
- `useWebGLFrameWaiter` がヘッドレスレンダー時に GPU 完了待ちを入れます。
13+
14+
```tsx
15+
import { useRef } from "react"
16+
import { useWebGLContext, useWebGLFrameWaiter } from "../src/lib/webgl"
17+
18+
const Canvas = () => {
19+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
20+
const { glRef } = useWebGLContext(canvasRef, ({ gl }) => {
21+
// シェーダやバッファを初期化
22+
return () => {
23+
// リソースの破棄
24+
}
25+
})
26+
27+
useWebGLFrameWaiter(glRef)
28+
29+
return <canvas ref={canvasRef} />
30+
}
31+
```
32+
33+
## ThreeCanvas
34+
35+
`<ThreeCanvas />``useAnimation` と組み合わせて使えます。
36+
37+
```tsx
38+
import { useAnimation, useVariable } from "../src/lib/animation"
39+
import { BEZIER_SMOOTH } from "../src/lib/animation/functions"
40+
import { seconds } from "../src/lib/frame"
41+
import { ThreeCanvas, THREE, disposeThreeObject } from "../src/lib/webgl/three"
42+
43+
const Scene = () => {
44+
const progress = useVariable(0)
45+
46+
useAnimation(async (ctx) => {
47+
await ctx.move(progress).to(1, seconds(2), BEZIER_SMOOTH)
48+
await ctx.move(progress).to(0, seconds(2), BEZIER_SMOOTH)
49+
}, [])
50+
51+
return (
52+
<ThreeCanvas
53+
setup={({ renderer, size }) => {
54+
renderer.outputColorSpace = THREE.SRGBColorSpace
55+
56+
const scene = new THREE.Scene()
57+
const camera = new THREE.PerspectiveCamera(45, size.cssWidth / size.cssHeight, 0.1, 100)
58+
camera.position.z = 6
59+
60+
const mesh = new THREE.Mesh(
61+
new THREE.BoxGeometry(),
62+
new THREE.MeshStandardMaterial({ color: 0x44aa88 })
63+
)
64+
scene.add(mesh)
65+
66+
const light = new THREE.DirectionalLight(0xffffff, 1)
67+
light.position.set(2, 3, 4)
68+
scene.add(light)
69+
70+
return {
71+
scene,
72+
camera,
73+
update: ({ frame }) => {
74+
const t = progress.get(frame)
75+
mesh.position.x = (t - 0.5) * 3
76+
mesh.rotation.y = t * Math.PI * 2
77+
},
78+
dispose: () => disposeThreeObject(mesh),
79+
}
80+
}}
81+
/>
82+
)
83+
}
84+
```

0 commit comments

Comments
 (0)