Browse Source

Merge pull request #21221 from mrdoob/editor

Editor: Added ffmpeg.wasm video renderer
Mr.doob 4 years ago
parent
commit
df63896cd2
6 changed files with 188 additions and 4 deletions
  1. 2 0
      editor/index.html
  2. 141 0
      editor/js/Sidebar.Project.Video.js
  3. 2 0
      editor/js/Sidebar.Project.js
  4. 16 3
      editor/js/libs/app.js
  5. 26 1
      editor/js/libs/ui.js
  6. 1 0
      editor/sw.js

+ 2 - 0
editor/index.html

@@ -11,6 +11,8 @@
 	<body>
 		<link rel="stylesheet" href="css/main.css">
 
+		<script src="https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js" defer></script>
+
 		<script src="../examples/js/libs/draco/draco_encoder.js"></script>
 
 		<link rel="stylesheet" href="js/libs/codemirror/codemirror.css">

+ 141 - 0
editor/js/Sidebar.Project.Video.js

@@ -0,0 +1,141 @@
+import { UIBreak, UIButton, UIInteger, UIPanel, UIProgress, UIRow, UIText } from './libs/ui.js';
+
+import { APP } from './libs/app.js';
+
+function SidebarProjectVideo( editor ) {
+
+	var container = new UIPanel();
+	container.setId( 'render' );
+
+	// Video
+
+	container.add( new UIText( 'Video' ).setTextTransform( 'uppercase' ) );
+	container.add( new UIBreak(), new UIBreak() );
+
+	// Resolution
+
+	var resolutionRow = new UIRow();
+	container.add( resolutionRow );
+
+	resolutionRow.add( new UIText( 'Resolution' ).setWidth( '90px' ) );
+
+	var videoWidth = new UIInteger( 600 ).setWidth( '28px' );
+	resolutionRow.add( videoWidth );
+
+	resolutionRow.add( new UIText( '×' ).setFontSize( '12px' ).setWidth( '14px' ) );
+
+	var videoHeight = new UIInteger( 600 ).setWidth( '28px' );
+	resolutionRow.add( videoHeight );
+
+	var videoFPS = new UIInteger( 30 ).setTextAlign( 'center' ).setWidth( '20px' );
+	resolutionRow.add( videoFPS );
+
+	resolutionRow.add( new UIText( 'fps' ).setFontSize( '12px' ) );
+
+	// Duration
+
+	var videoDurationRow = new UIRow();
+	videoDurationRow.add( new UIText( 'Duration' ).setWidth( '90px' ) );
+
+	var videoDuration = new UIInteger( 10 );
+	videoDurationRow.add( videoDuration );
+
+	container.add( videoDurationRow );
+
+	// Render
+
+	container.add( new UIText( '' ).setWidth( '90px' ) );
+
+	const progress = new UIProgress( 0 );
+	progress.setDisplay( 'none' );
+	progress.setWidth( '170px' );
+	container.add( progress );
+
+	const renderButton = new UIButton( 'RENDER' );
+	renderButton.setWidth( '170px' );
+	renderButton.onClick( async () => {
+
+		renderButton.setDisplay( 'none' );
+		progress.setDisplay( '' );
+		progress.setValue( 0 );
+
+		const player = new APP.Player();
+		player.load( editor.toJSON() );
+		player.setPixelRatio( 1 );
+		player.setSize( videoWidth.getValue(), videoHeight.getValue() );
+
+		const canvas = player.dom.firstElementChild;
+
+		//
+
+		const { createFFmpeg, fetchFile } = FFmpeg; // eslint-disable-line no-undef
+		const ffmpeg = createFFmpeg( { log: true } );
+
+		await ffmpeg.load();
+
+		ffmpeg.setProgress( ( { ratio } ) => {
+
+			progress.setValue( ratio );
+
+		} );
+
+		const fps = videoFPS.getValue();
+		const duration = videoDuration.getValue();
+		const frames = duration * fps;
+
+		let currentTime = 0;
+
+		for ( let i = 0; i < frames; i ++ ) {
+
+			player.render( currentTime );
+
+			const num = i.toString().padStart( 5, '0' );
+			ffmpeg.FS( 'writeFile', `tmp.${num}.png`, await fetchFile( canvas.toDataURL() ) );
+			currentTime += 1 / fps;
+
+			progress.setValue( i / frames );
+
+		}
+
+		await ffmpeg.run( '-framerate', String( fps ), '-pattern_type', 'glob', '-i', '*.png', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', String( 6 ), 'out.mp4' );
+
+		const data = ffmpeg.FS( 'readFile', 'out.mp4' );
+
+		for ( let i = 0; i < frames; i ++ ) {
+
+			const num = i.toString().padStart( 5, '0' );
+			ffmpeg.FS( 'unlink', `tmp.${num}.png` );
+
+		}
+
+		save( new Blob( [ data.buffer ], { type: 'video/mp4' } ), 'out.mp4' );
+
+		player.dispose();
+
+		renderButton.setDisplay( '' );
+		progress.setDisplay( 'none' );
+
+	} );
+	container.add( renderButton );
+
+	// SAVE
+
+	const link = document.createElement( 'a' );
+
+	function save( blob, filename ) {
+
+		link.href = URL.createObjectURL( blob );
+		link.download = filename;
+		link.dispatchEvent( new MouseEvent( 'click' ) );
+
+		// URL.revokeObjectURL( url ); breaks Firefox...
+
+	}
+
+	//
+
+	return container;
+
+}
+
+export { SidebarProjectVideo };

+ 2 - 0
editor/js/Sidebar.Project.js

@@ -2,6 +2,7 @@ import { UIPanel, UIRow, UIInput, UICheckbox, UIText, UISpan } from './libs/ui.j
 
 /* import { SidebarProjectMaterials } from './Sidebar.Project.Materials.js'; */
 import { SidebarProjectRenderer } from './Sidebar.Project.Renderer.js';
+import { SidebarProjectVideo } from './Sidebar.Project.Video.js';
 
 function SidebarProject( editor ) {
 
@@ -61,6 +62,7 @@ function SidebarProject( editor ) {
 
 	/* container.add( new SidebarProjectMaterials( editor ) ); */
 	container.add( new SidebarProjectRenderer( editor ) );
+	container.add( new SidebarProjectVideo( editor ) );
 
 	return container;
 

+ 16 - 3
editor/js/libs/app.js

@@ -1,16 +1,15 @@
-
 var APP = {
 
 	Player: function () {
 
 		var renderer = new THREE.WebGLRenderer( { antialias: true } );
-		renderer.setPixelRatio( window.devicePixelRatio );
+		renderer.setPixelRatio( window.devicePixelRatio ); // TODO: Use player.setPixelRatio()
 		renderer.outputEncoding = THREE.sRGBEncoding;
 
 		var loader = new THREE.ObjectLoader();
 		var camera, scene;
 
-		var vrButton = VRButton.createButton( renderer );
+		var vrButton = VRButton.createButton( renderer ); // eslint-disable-line no-undef
 
 		var events = {};
 
@@ -116,6 +115,12 @@ var APP = {
 
 		};
 
+		this.setPixelRatio = function ( pixelRatio ) {
+
+			renderer.setPixelRatio( pixelRatio );
+
+		};
+
 		this.setSize = function ( width, height ) {
 
 			this.width = width;
@@ -202,6 +207,14 @@ var APP = {
 
 		};
 
+		this.render = function ( time ) {
+
+			dispatch( events.update, { time: time * 1000, delta: 0 /* TODO */ } );
+
+			renderer.render( scene, camera );
+
+		};
+
 		this.dispose = function () {
 
 			renderer.dispose();

+ 26 - 1
editor/js/libs/ui.js

@@ -1069,6 +1069,31 @@ UIButton.prototype.setLabel = function ( value ) {
 };
 
 
+// UIProgress
+
+function UIProgress( value ) {
+
+	UIElement.call( this );
+
+	var dom = document.createElement( 'progress' );
+
+	this.dom = dom;
+	this.dom.value = value;
+
+	return this;
+
+}
+
+UIProgress.prototype = Object.create( UIElement.prototype );
+UIProgress.prototype.constructor = UIProgress;
+
+UIProgress.prototype.setValue = function ( value ) {
+
+	this.dom.value = value;
+
+};
+
+
 // UITabbedPanel
 
 function UITabbedPanel( ) {
@@ -1351,4 +1376,4 @@ UIListbox.ListboxItem = function ( parent ) {
 UIListbox.ListboxItem.prototype = Object.create( UIElement.prototype );
 UIListbox.ListboxItem.prototype.constructor = UIListbox.ListboxItem;
 
-export { UIElement, UISpan, UIDiv, UIRow, UIPanel, UIText, UIInput, UITextArea, UISelect, UICheckbox, UIColor, UINumber, UIInteger, UIBreak, UIHorizontalRule, UIButton, UITabbedPanel, UIListbox };
+export { UIElement, UISpan, UIDiv, UIRow, UIPanel, UIText, UIInput, UITextArea, UISelect, UICheckbox, UIColor, UINumber, UIInteger, UIBreak, UIHorizontalRule, UIButton, UIProgress, UITabbedPanel, UIListbox };

+ 1 - 0
editor/sw.js

@@ -134,6 +134,7 @@ const assets = [
 	'./js/Sidebar.Project.js',
 	'./js/Sidebar.Project.Materials.js',
 	'./js/Sidebar.Project.Renderer.js',
+	'./js/Sidebar.Project.Video.js',
 	'./js/Sidebar.Settings.js',
 	'./js/Sidebar.Settings.History.js',
 	'./js/Sidebar.Settings.Shortcuts.js',